diff --git a/CMakeLists.txt b/CMakeLists.txt index 3db558a870ec..9fb9304d0c9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,7 @@ if("${CMAKE_BUILD_TYPE}" STREQUAL "Release" AND NOT WIN32 AND CMAKE_COMPILER_IS_ endif() #Activate C++11 -set(CMAKE_CXX_STANDARD 11) # Upgrading to C++17 would need to remove usage of bind2nd (should be easy). +set(CMAKE_CXX_STANDARD 11) # Upgrading to C++17 should be tried. set(CMAKE_CXX_STANDARD_REQUIRED ON) # Mark some warnings as errors diff --git a/Core/GDCore/Project/ExternalLayout.h b/Core/GDCore/Project/ExternalLayout.h index 2a18d5ce152a..6c25db8f2d6c 100644 --- a/Core/GDCore/Project/ExternalLayout.h +++ b/Core/GDCore/Project/ExternalLayout.h @@ -96,19 +96,6 @@ class GD_CORE_API ExternalLayout { gd::String associatedLayout; }; -/** - * \brief Functor testing ExternalLayout' name - */ -struct ExternalLayoutHasName - : public std::binary_function, - gd::String, - bool> { - bool operator()(const std::unique_ptr& externalLayout, - gd::String name) const { - return externalLayout->GetName() == name; - } -}; - } // namespace gd #endif // GDCORE_EXTERNALLAYOUT_H diff --git a/Core/GDCore/Project/InitialInstance.cpp b/Core/GDCore/Project/InitialInstance.cpp index 720ad5e3f4af..31c45b28fdda 100644 --- a/Core/GDCore/Project/InitialInstance.cpp +++ b/Core/GDCore/Project/InitialInstance.cpp @@ -27,7 +27,11 @@ InitialInstance::InitialInstance() rotationX(0), rotationY(0), zOrder(0), + opacity(255), layer(""), + flippedX(false), + flippedY(false), + flippedZ(false), customSize(false), customDepth(false), width(0), @@ -57,7 +61,11 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) { SetHasCustomDepth(false); } SetZOrder(element.GetIntAttribute("zOrder", 0, "plan")); + SetOpacity(element.GetIntAttribute("opacity", 255)); SetLayer(element.GetStringAttribute("layer")); + SetFlippedX(element.GetBoolAttribute("flippedX", false)); + SetFlippedY(element.GetBoolAttribute("flippedY", false)); + SetFlippedZ(element.GetBoolAttribute("flippedZ", false)); SetLocked(element.GetBoolAttribute("locked", false)); SetSealed(element.GetBoolAttribute("sealed", false)); SetShouldKeepRatio(element.GetBoolAttribute("keepRatio", false)); @@ -113,6 +121,10 @@ void InitialInstance::SerializeTo(SerializerElement& element) const { element.SetAttribute("y", GetY()); if (GetZ() != 0) element.SetAttribute("z", GetZ()); element.SetAttribute("zOrder", GetZOrder()); + if (GetOpacity() != 255) element.SetAttribute("opacity", GetOpacity()); + if (IsFlippedX()) element.SetAttribute("flippedX", IsFlippedX()); + if (IsFlippedY()) element.SetAttribute("flippedY", IsFlippedY()); + if (IsFlippedZ()) element.SetAttribute("flippedZ", IsFlippedZ()); element.SetAttribute("layer", GetLayer()); element.SetAttribute("angle", GetAngle()); if (GetRotationX() != 0) element.SetAttribute("rotationX", GetRotationX()); @@ -155,8 +167,8 @@ InitialInstance& InitialInstance::ResetPersistentUuid() { std::map InitialInstance::GetCustomProperties( - gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer) { + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer) { // Find an object if (objectsContainer.HasObjectNamed(GetObjectName())) return objectsContainer.GetObject(GetObjectName()) @@ -172,9 +184,10 @@ InitialInstance::GetCustomProperties( } bool InitialInstance::UpdateCustomProperty( - const gd::String &name, const gd::String &value, - gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer) { + const gd::String& name, + const gd::String& value, + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer) { if (objectsContainer.HasObjectNamed(GetObjectName())) return objectsContainer.GetObject(GetObjectName()) .GetConfiguration() diff --git a/Core/GDCore/Project/InitialInstance.h b/Core/GDCore/Project/InitialInstance.h index bb8a423d4381..e521d3dcba1c 100644 --- a/Core/GDCore/Project/InitialInstance.h +++ b/Core/GDCore/Project/InitialInstance.h @@ -29,7 +29,7 @@ class GD_CORE_API InitialInstance { * \brief Create an initial instance pointing to no object, at position (0,0). */ InitialInstance(); - virtual ~InitialInstance(){}; + virtual ~InitialInstance() {}; /** * Must return a pointer to a copy of the object. A such method is needed to @@ -123,6 +123,46 @@ class GD_CORE_API InitialInstance { */ void SetZOrder(int zOrder_) { zOrder = zOrder_; } + /** + * \brief Get Opacity. + */ + int GetOpacity() const { return opacity; } + + /** + * \brief Set the opacity of the instance. + */ + void SetOpacity(int opacity_) { opacity = opacity_; } + + /** + * \brief Return true if the instance is flipped on X axis. + */ + bool IsFlippedX() const { return flippedX; } + + /** + * \brief Set whether the instance is flipped on X axis. + */ + void SetFlippedX(bool flippedX_) { flippedX = flippedX_; } + + /** + * \brief Return true if the instance is flipped on Y axis. + */ + bool IsFlippedY() const { return flippedY; } + + /** + * \brief Set whether the instance is flipped on Y axis. + */ + void SetFlippedY(bool flippedY_) { flippedY = flippedY_; } + + /** + * \brief Return true if the instance is flipped on Z axis. + */ + bool IsFlippedZ() const { return flippedZ; } + + /** + * \brief Set whether the instance is flipped on Z axis. + */ + void SetFlippedZ(bool flippedZ_) { flippedZ = flippedZ_; } + /** * \brief Get the layer the instance belongs to. */ @@ -134,8 +174,9 @@ class GD_CORE_API InitialInstance { void SetLayer(const gd::String& layer_) { layer = layer_; } /** - * \brief Return true if the instance has a width/height which is different from its - * object default width/height. This is independent from `HasCustomDepth`. + * \brief Return true if the instance has a width/height which is different + * from its object default width/height. This is independent from + * `HasCustomDepth`. * * \see gd::Object */ @@ -150,15 +191,13 @@ class GD_CORE_API InitialInstance { bool HasCustomDepth() const { return customDepth; } /** - * \brief Set whether the instance has a width/height which is different from its - * object default width/height or not. - * This is independent from `SetHasCustomDepth`. + * \brief Set whether the instance has a width/height which is different from + * its object default width/height or not. This is independent from + * `SetHasCustomDepth`. * * \see gd::Object */ - void SetHasCustomSize(bool hasCustomSize_) { - customSize = hasCustomSize_; - } + void SetHasCustomSize(bool hasCustomSize_) { customSize = hasCustomSize_; } /** * \brief Set whether the instance has a depth which is different from its @@ -264,18 +303,19 @@ class GD_CORE_API InitialInstance { * \note Common properties ( name, position... ) do not need to be * inserted in this map */ - std::map - GetCustomProperties(gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer); + std::map GetCustomProperties( + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer); /** * \brief Update the property called \a name with the new \a value. * * \return false if the property could not be updated. */ - bool UpdateCustomProperty(const gd::String &name, const gd::String &value, - gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer); + bool UpdateCustomProperty(const gd::String& name, + const gd::String& value, + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer); /** * \brief Get the value of a double property stored in the instance. @@ -343,6 +383,10 @@ class GD_CORE_API InitialInstance { double rotationX; ///< Instance angle on X axis (for a 3D object) double rotationY; ///< Instance angle on Y axis (for a 3D object) int zOrder; ///< Instance Z order (for a 2D object) + int opacity; ///< Instance opacity + bool flippedX; ///< True if the instance is flipped on X axis + bool flippedY; ///< True if the instance is flipped on Y axis + bool flippedZ; ///< True if the instance is flipped on Z axis gd::String layer; ///< Instance layer bool customSize; ///< True if object has a custom width and height bool customDepth; ///< True if object has a custom depth @@ -352,13 +396,13 @@ class GD_CORE_API InitialInstance { gd::VariablesContainer initialVariables; ///< Instance specific variables bool locked; ///< True if the instance is locked bool sealed; ///< True if the instance is sealed - bool keepRatio; ///< True if the instance's dimensions - /// should keep the same ratio. + bool keepRatio; ///< True if the instance's dimensions + /// should keep the same ratio. mutable gd::String persistentUuid; ///< A persistent random version 4 UUID, /// useful for hot reloading. - static gd::String* - badStringPropertyValue; ///< Empty string returned by GetRawStringProperty + static gd::String* badStringPropertyValue; ///< Empty string returned by + ///< GetRawStringProperty }; } // namespace gd diff --git a/Core/GDCore/Project/Layout.h b/Core/GDCore/Project/Layout.h index ffdfce81dc6d..f6cbfd470863 100644 --- a/Core/GDCore/Project/Layout.h +++ b/Core/GDCore/Project/Layout.h @@ -405,18 +405,6 @@ class GD_CORE_API Layout { const gd::String& behaviorsType); }; -/** - * \brief Functor testing layout name. - * \see gd::Layout - */ -struct LayoutHasName - : public std::binary_function, gd::String, bool> { - bool operator()(const std::unique_ptr& layout, - gd::String name) const { - return layout->GetName() == name; - } -}; - /** * \brief Get the names of all layers from the given layout * that are invisible. diff --git a/Core/GDCore/Project/Project.cpp b/Core/GDCore/Project/Project.cpp index 4673820d1374..77849455ffaa 100644 --- a/Core/GDCore/Project/Project.cpp +++ b/Core/GDCore/Project/Project.cpp @@ -264,15 +264,21 @@ bool Project::RemovePlatform(const gd::String& platformName) { bool Project::HasLayoutNamed(const gd::String& name) const { return (find_if(scenes.begin(), scenes.end(), - bind2nd(gd::LayoutHasName(), name)) != scenes.end()); + [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + }) != scenes.end()); } gd::Layout& Project::GetLayout(const gd::String& name) { return *(*find_if( - scenes.begin(), scenes.end(), bind2nd(gd::LayoutHasName(), name))); + scenes.begin(), scenes.end(), [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + })); } const gd::Layout& Project::GetLayout(const gd::String& name) const { return *(*find_if( - scenes.begin(), scenes.end(), bind2nd(gd::LayoutHasName(), name))); + scenes.begin(), scenes.end(), [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + })); } gd::Layout& Project::GetLayout(std::size_t index) { return *scenes[index]; } const gd::Layout& Project::GetLayout(std::size_t index) const { @@ -317,7 +323,9 @@ gd::Layout& Project::InsertLayout(const gd::Layout& layout, void Project::RemoveLayout(const gd::String& name) { std::vector >::iterator scene = - find_if(scenes.begin(), scenes.end(), bind2nd(gd::LayoutHasName(), name)); + find_if(scenes.begin(), scenes.end(), [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + }); if (scene == scenes.end()) return; scenes.erase(scene); @@ -326,19 +334,24 @@ void Project::RemoveLayout(const gd::String& name) { bool Project::HasExternalEventsNamed(const gd::String& name) const { return (find_if(externalEvents.begin(), externalEvents.end(), - bind2nd(gd::ExternalEventsHasName(), name)) != - externalEvents.end()); + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + }) != externalEvents.end()); } gd::ExternalEvents& Project::GetExternalEvents(const gd::String& name) { return *(*find_if(externalEvents.begin(), externalEvents.end(), - bind2nd(gd::ExternalEventsHasName(), name))); + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + })); } const gd::ExternalEvents& Project::GetExternalEvents( const gd::String& name) const { return *(*find_if(externalEvents.begin(), externalEvents.end(), - bind2nd(gd::ExternalEventsHasName(), name))); + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + })); } gd::ExternalEvents& Project::GetExternalEvents(std::size_t index) { return *externalEvents[index]; @@ -382,7 +395,9 @@ void Project::RemoveExternalEvents(const gd::String& name) { std::vector >::iterator events = find_if(externalEvents.begin(), externalEvents.end(), - bind2nd(gd::ExternalEventsHasName(), name)); + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + }); if (events == externalEvents.end()) return; externalEvents.erase(events); @@ -448,19 +463,24 @@ void Project::SwapExternalLayouts(std::size_t first, std::size_t second) { bool Project::HasExternalLayoutNamed(const gd::String& name) const { return (find_if(externalLayouts.begin(), externalLayouts.end(), - bind2nd(gd::ExternalLayoutHasName(), name)) != - externalLayouts.end()); + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + }) != externalLayouts.end()); } gd::ExternalLayout& Project::GetExternalLayout(const gd::String& name) { return *(*find_if(externalLayouts.begin(), externalLayouts.end(), - bind2nd(gd::ExternalLayoutHasName(), name))); + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + })); } const gd::ExternalLayout& Project::GetExternalLayout( const gd::String& name) const { return *(*find_if(externalLayouts.begin(), externalLayouts.end(), - bind2nd(gd::ExternalLayoutHasName(), name))); + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + })); } gd::ExternalLayout& Project::GetExternalLayout(std::size_t index) { return *externalLayouts[index]; @@ -504,7 +524,9 @@ void Project::RemoveExternalLayout(const gd::String& name) { std::vector >::iterator externalLayout = find_if(externalLayouts.begin(), externalLayouts.end(), - bind2nd(gd::ExternalLayoutHasName(), name)); + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + }); if (externalLayout == externalLayouts.end()) return; externalLayouts.erase(externalLayout); @@ -1076,7 +1098,9 @@ bool Project::HasSourceFile(gd::String name, gd::String language) const { vector >::const_iterator sourceFile = find_if(externalSourceFiles.begin(), externalSourceFiles.end(), - bind2nd(gd::ExternalSourceFileHasName(), name)); + [&name](const std::unique_ptr& sourceFile) { + return sourceFile->GetFileName() == name; + }); if (sourceFile == externalSourceFiles.end()) return false; @@ -1086,20 +1110,26 @@ bool Project::HasSourceFile(gd::String name, gd::String language) const { gd::SourceFile& Project::GetSourceFile(const gd::String& name) { return *(*find_if(externalSourceFiles.begin(), externalSourceFiles.end(), - bind2nd(gd::ExternalSourceFileHasName(), name))); + [&name](const std::unique_ptr& sourceFile) { + return sourceFile->GetFileName() == name; + })); } const gd::SourceFile& Project::GetSourceFile(const gd::String& name) const { return *(*find_if(externalSourceFiles.begin(), externalSourceFiles.end(), - bind2nd(gd::ExternalSourceFileHasName(), name))); + [&name](const std::unique_ptr& sourceFile) { + return sourceFile->GetFileName() == name; + })); } void Project::RemoveSourceFile(const gd::String& name) { std::vector >::iterator sourceFile = find_if(externalSourceFiles.begin(), externalSourceFiles.end(), - bind2nd(gd::ExternalSourceFileHasName(), name)); + [&name](const std::unique_ptr& sourceFile) { + return sourceFile->GetFileName() == name; + }); if (sourceFile == externalSourceFiles.end()) return; externalSourceFiles.erase(sourceFile); diff --git a/Core/GDCore/Project/SourceFile.h b/Core/GDCore/Project/SourceFile.h index 25bd9f9ad02b..bf01f99ecb9f 100644 --- a/Core/GDCore/Project/SourceFile.h +++ b/Core/GDCore/Project/SourceFile.h @@ -87,20 +87,6 @@ class GD_CORE_API SourceFile { ///< SetAssociatedEvent. }; -//"Tool" Functions - -/** - * Functor testing Source Files name - */ -struct ExternalSourceFileHasName - : public std:: - binary_function, gd::String, bool> { - bool operator()(const std::unique_ptr& externalEvents, - gd::String name) const { - return externalEvents->GetFileName() == name; - } -}; - } // namespace gd #endif // SOURCEFILE_H diff --git a/Extensions/3D/A_RuntimeObject3D.ts b/Extensions/3D/A_RuntimeObject3D.ts index 4e6b4198099a..f57067cb5aad 100644 --- a/Extensions/3D/A_RuntimeObject3D.ts +++ b/Extensions/3D/A_RuntimeObject3D.ts @@ -164,8 +164,18 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } - if (initialInstanceData.depth !== undefined) + if (initialInstanceData.depth !== undefined) { this.setDepth(initialInstanceData.depth); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } + if (initialInstanceData.flippedZ) { + this.flipZ(initialInstanceData.flippedZ); + } } setX(x: float): void { diff --git a/Extensions/3D/CustomRuntimeObject3D.ts b/Extensions/3D/CustomRuntimeObject3D.ts index 1bdf7936c994..906cd55750fe 100644 --- a/Extensions/3D/CustomRuntimeObject3D.ts +++ b/Extensions/3D/CustomRuntimeObject3D.ts @@ -72,8 +72,18 @@ namespace gdjs { extraInitializationFromInitialInstance(initialInstanceData: InstanceData) { super.extraInitializationFromInitialInstance(initialInstanceData); - if (initialInstanceData.depth !== undefined) + if (initialInstanceData.depth !== undefined) { this.setDepth(initialInstanceData.depth); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } + if (initialInstanceData.flippedZ) { + this.flipZ(initialInstanceData.flippedZ); + } } /** diff --git a/Extensions/3D/JsExtension.js b/Extensions/3D/JsExtension.js index 9234593e7fd8..6034c28b57fa 100644 --- a/Extensions/3D/JsExtension.js +++ b/Extensions/3D/JsExtension.js @@ -2216,8 +2216,14 @@ module.exports = { this._centerY / objectTextureFrame.height; this._pixiTexturedObject.angle = this._instance.getAngle(); - this._pixiTexturedObject.scale.x = width / objectTextureFrame.width; - this._pixiTexturedObject.scale.y = height / objectTextureFrame.height; + const scaleX = + (width / objectTextureFrame.width) * + (this._instance.isFlippedX() ? -1 : 1); + const scaleY = + (height / objectTextureFrame.height) * + (this._instance.isFlippedY() ? -1 : 1); + this._pixiTexturedObject.scale.x = scaleX; + this._pixiTexturedObject.scale.y = scaleY; this._pixiTexturedObject.position.x = this._instance.getX() + @@ -2244,6 +2250,9 @@ module.exports = { this._pixiFallbackObject.position.y = this._instance.getY() + height / 2; this._pixiFallbackObject.angle = this._instance.getAngle(); + + if (this._instance.isFlippedX()) this._pixiFallbackObject.scale.x = -1; + if (this._instance.isFlippedY()) this._pixiFallbackObject.scale.y = -1; } update() { @@ -2393,12 +2402,16 @@ module.exports = { RenderedInstance.toRad(this._instance.getAngle()) ); + const scaleX = width * (this._instance.isFlippedX() ? -1 : 1); + const scaleY = height * (this._instance.isFlippedY() ? -1 : 1); + const scaleZ = depth * (this._instance.isFlippedZ() ? -1 : 1); + if ( - width !== this._threeObject.scale.width || - height !== this._threeObject.scale.height || - depth !== this._threeObject.scale.depth + scaleX !== this._threeObject.scale.width || + scaleY !== this._threeObject.scale.height || + scaleZ !== this._threeObject.scale.depth ) { - this._threeObject.scale.set(width, height, depth); + this._threeObject.scale.set(scaleX, scaleY, scaleZ); this.updateTextureUvMapping(); } } @@ -3186,12 +3199,16 @@ module.exports = { RenderedInstance.toRad(this._instance.getAngle()) ); + const scaleX = width * (this._instance.isFlippedX() ? -1 : 1); + const scaleY = height * (this._instance.isFlippedY() ? -1 : 1); + const scaleZ = depth * (this._instance.isFlippedZ() ? -1 : 1); + if ( - width !== this._threeObject.scale.width || - height !== this._threeObject.scale.height || - depth !== this._threeObject.scale.depth + scaleX !== this._threeObject.scale.width || + scaleY !== this._threeObject.scale.height || + scaleZ !== this._threeObject.scale.depth ) { - this._threeObject.scale.set(width, height, depth); + this._threeObject.scale.set(scaleX, scaleY, scaleZ); } } diff --git a/Extensions/BBText/JsExtension.js b/Extensions/BBText/JsExtension.js index cfe9d1b5bce3..f427718c089d 100644 --- a/Extensions/BBText/JsExtension.js +++ b/Extensions/BBText/JsExtension.js @@ -66,13 +66,6 @@ module.exports = { .setLabel(_('Base color')) .setGroup(_('Appearance')); - objectProperties - .getOrCreate('opacity') - .setValue(objectContent.opacity.toString()) - .setType('number') - .setLabel(_('Opacity (0-255)')) - .setGroup(_('Appearance')); - objectProperties .getOrCreate('fontSize') .setValue(objectContent.fontSize.toString()) @@ -545,9 +538,6 @@ module.exports = { this._pixiObject.text = rawText; } - const opacity = +properties.get('opacity').getValue(); - this._pixiObject.alpha = opacity / 255; - const color = properties.get('color').getValue(); const newColor = objectsRenderingService.rgbOrHexToHexNumber(color); if (newColor !== this._pixiObject.textStyles.default.fill) { @@ -615,6 +605,13 @@ module.exports = { this._pixiObject.dirty = true; } } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } /** diff --git a/Extensions/BBText/bbtextruntimeobject.ts b/Extensions/BBText/bbtextruntimeobject.ts index b95229e7ce84..b87869c55171 100644 --- a/Extensions/BBText/bbtextruntimeobject.ts +++ b/Extensions/BBText/bbtextruntimeobject.ts @@ -192,6 +192,9 @@ namespace gdjs { 250 ); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onDestroyed(): void { diff --git a/Extensions/BitmapText/JsExtension.js b/Extensions/BitmapText/JsExtension.js index badb02cd9593..d06142f6ed2d 100644 --- a/Extensions/BitmapText/JsExtension.js +++ b/Extensions/BitmapText/JsExtension.js @@ -59,13 +59,6 @@ module.exports = { .setType('textarea') .setLabel(_('Text')); - objectProperties - .getOrCreate('opacity') - .setValue(objectContent.opacity.toString()) - .setType('number') - .setLabel(_('Opacity (0-255)')) - .setGroup(_('Appearance')); - objectProperties .getOrCreate('align') .setValue(objectContent.align) @@ -673,9 +666,6 @@ module.exports = { const rawText = properties.get('text').getValue(); this._pixiObject.text = rawText; - const opacity = +properties.get('opacity').getValue(); - this._pixiObject.alpha = opacity / 255; - const align = properties.get('align').getValue(); this._pixiObject.align = align; @@ -739,6 +729,13 @@ module.exports = { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } onRemovedFromScene() { diff --git a/Extensions/BitmapText/bitmaptextruntimeobject.ts b/Extensions/BitmapText/bitmaptextruntimeobject.ts index b9ff8043e30b..3804cd91459d 100644 --- a/Extensions/BitmapText/bitmaptextruntimeobject.ts +++ b/Extensions/BitmapText/bitmaptextruntimeobject.ts @@ -203,6 +203,9 @@ namespace gdjs { if (initialInstanceData.customSize) { this.setWrappingWidth(initialInstanceData.width); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onDestroyed(): void { diff --git a/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts b/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts index 3cbeec2519ff..74696391b514 100644 --- a/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts +++ b/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts @@ -172,6 +172,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } /** diff --git a/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts b/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts index 766a33e0a7aa..dc4eba40f7e3 100644 --- a/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts +++ b/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts @@ -166,6 +166,19 @@ namespace gdjs { return true; } + /** + * Initialize the extra parameters that could be set for an instance. + * @param initialInstanceData The extra parameters + */ + extraInitializationFromInitialInstance(initialInstanceData: InstanceData) { + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } + } + stepBehaviorsPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) { //We redefine stepBehaviorsPreEvents just to clear the graphics before running events. if (this._clearBetweenFrames) { diff --git a/Extensions/Spine/JsExtension.js b/Extensions/Spine/JsExtension.js index 787b0b01fba8..326622790a27 100644 --- a/Extensions/Spine/JsExtension.js +++ b/Extensions/Spine/JsExtension.js @@ -182,6 +182,20 @@ module.exports = { this._instance.getAngle() ); + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; + // Scale is already handled below, so we just apply the flip here. + this._pixiObject.scale.x = + Math.abs(this._pixiObject.scale.x) * + (this._instance.isFlippedX() ? -1 : 1); + this._pixiObject.scale.y = + Math.abs(this._pixiObject.scale.y) * + (this._instance.isFlippedY() ? -1 : 1); + this.setAnimation(this._instance.getRawDoubleProperty('animation')); const width = this.getWidth(); diff --git a/Extensions/Spine/spineruntimeobject.ts b/Extensions/Spine/spineruntimeobject.ts index 09d37f1c993a..3c07188eb533 100644 --- a/Extensions/Spine/spineruntimeobject.ts +++ b/Extensions/Spine/spineruntimeobject.ts @@ -208,6 +208,15 @@ namespace gdjs { this.setSize(initialInstanceData.width, initialInstanceData.height); this.invalidateHitboxes(); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } } getDrawableX(): number { diff --git a/Extensions/TextInput/JsExtension.js b/Extensions/TextInput/JsExtension.js index 5ba27af39092..7aa37e7b2d68 100644 --- a/Extensions/TextInput/JsExtension.js +++ b/Extensions/TextInput/JsExtension.js @@ -789,6 +789,13 @@ module.exports = { ); this._pixiGraphics.drawRect(0, 0, width, height); this._pixiGraphics.endFill(); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { diff --git a/Extensions/TextInput/textinputruntimeobject.ts b/Extensions/TextInput/textinputruntimeobject.ts index f27fa2fbe15e..9e3a54c79b8e 100644 --- a/Extensions/TextInput/textinputruntimeobject.ts +++ b/Extensions/TextInput/textinputruntimeobject.ts @@ -254,6 +254,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onScenePaused(runtimeScene: gdjs.RuntimeScene): void { diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts index 9fcd5897db0d..01a63207f617 100644 --- a/Extensions/TextObject/textruntimeobject.ts +++ b/Extensions/TextObject/textruntimeobject.ts @@ -328,6 +328,9 @@ namespace gdjs { } else { this.setWrapping(false); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } /** diff --git a/Extensions/TileMap/JsExtension.js b/Extensions/TileMap/JsExtension.js index 362fc4e2133d..e80702f63363 100644 --- a/Extensions/TileMap/JsExtension.js +++ b/Extensions/TileMap/JsExtension.js @@ -1611,6 +1611,7 @@ module.exports = { this.tileMapPixiObject = new Tilemap.CompositeTilemap(); this._pixiObject = this.tileMapPixiObject; + this._editableTileMap = null; // Implement `containsPoint` so that we can set `interactive` to true and // the Tilemap will properly emit events when hovered/clicked. @@ -1686,35 +1687,27 @@ module.exports = { * This is used to reload the Tilemap */ updateTileMap() { + const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties(); + // Get the tileset resource to use - const tilemapAtlasImage = this._associatedObjectConfiguration - .getProperties() + const tilemapAtlasImage = tilemapObjectProperties .get('tilemapAtlasImage') .getValue(); - const tilemapJsonFile = this._associatedObjectConfiguration - .getProperties() + const tilemapJsonFile = tilemapObjectProperties .get('tilemapJsonFile') .getValue(); - const tilesetJsonFile = this._associatedObjectConfiguration - .getProperties() + const tilesetJsonFile = tilemapObjectProperties .get('tilesetJsonFile') .getValue(); const layerIndex = parseInt( - this._associatedObjectConfiguration - .getProperties() - .get('layerIndex') - .getValue(), + tilemapObjectProperties.get('layerIndex').getValue(), 10 ); const levelIndex = parseInt( - this._associatedObjectConfiguration - .getProperties() - .get('levelIndex') - .getValue(), + tilemapObjectProperties.get('levelIndex').getValue(), 10 ); - const displayMode = this._associatedObjectConfiguration - .getProperties() + const displayMode = tilemapObjectProperties .get('displayMode') .getValue(); @@ -1755,6 +1748,8 @@ module.exports = { return; } + this._editableTileMap = tileMap; + /** @type {TileMapHelper.TileTextureCache} */ manager.getOrLoadTextureCache( this._loadTileMapWithCallback.bind(this), @@ -1774,12 +1769,13 @@ module.exports = { return; } this._onLoadingSuccess(); + if (!this._editableTileMap) return; - this.width = tileMap.getWidth(); - this.height = tileMap.getHeight(); + this.width = this._editableTileMap.getWidth(); + this.height = this._editableTileMap.getHeight(); TilemapHelper.PixiTileMapHelper.updatePixiTileMap( this.tileMapPixiObject, - tileMap, + this._editableTileMap, textureCache, displayMode, layerIndex @@ -1800,6 +1796,85 @@ module.exports = { } } + /** + * This is called to update the PIXI object on the scene editor, without reloading the tilemap. + */ + updatePixiTileMap() { + const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties(); + + // Get the tileset resource to use + const tilemapAtlasImage = tilemapObjectProperties + .get('tilemapAtlasImage') + .getValue(); + const tilemapJsonFile = tilemapObjectProperties + .get('tilemapJsonFile') + .getValue(); + const tilesetJsonFile = tilemapObjectProperties + .get('tilesetJsonFile') + .getValue(); + const layerIndex = parseInt( + tilemapObjectProperties.get('layerIndex').getValue(), + 10 + ); + const levelIndex = parseInt( + tilemapObjectProperties.get('levelIndex').getValue(), + 10 + ); + const displayMode = tilemapObjectProperties + .get('displayMode') + .getValue(); + + const tilemapResource = this._project + .getResourcesManager() + .getResource(tilemapJsonFile); + + let metadata = {}; + try { + const tilemapMetadataAsString = tilemapResource.getMetadata(); + if (tilemapMetadataAsString) + metadata = JSON.parse(tilemapMetadataAsString); + } catch (error) { + console.warn('Malformed metadata in a tilemap object:', error); + } + const mapping = metadata.embeddedResourcesMapping || {}; + + /** @type {TileMapHelper.TileMapManager} */ + const manager = TilemapHelper.TileMapManager.getManager(this._project); + + /** @type {TileMapHelper.TileTextureCache} */ + manager.getOrLoadTextureCache( + this._loadTileMapWithCallback.bind(this), + (textureName) => + this._pixiResourcesLoader.getPIXITexture( + this._project, + mapping[textureName] || textureName + ), + tilemapAtlasImage, + tilemapJsonFile, + tilesetJsonFile, + levelIndex, + (textureCache) => { + if (!textureCache) { + this._onLoadingError(); + // getOrLoadTextureCache already log warns and errors. + return; + } + this._onLoadingSuccess(); + if (!this._editableTileMap) return; + + this.width = this._editableTileMap.getWidth(); + this.height = this._editableTileMap.getHeight(); + TilemapHelper.PixiTileMapHelper.updatePixiTileMap( + this.tileMapPixiObject, + this._editableTileMap, + textureCache, + displayMode, + layerIndex + ); + } + ); + } + // GDJS doesn't use Promise to avoid allocation. _loadTileMapWithCallback(tilemapJsonFile, tilesetJsonFile, callback) { this._loadTileMap(tilemapJsonFile, tilesetJsonFile).then(callback); @@ -1870,6 +1945,30 @@ module.exports = { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Update the opacity, if needed. + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + + if ( + this._editableTileMap && + this._pixiObject.alpha !== alphaForDisplay + ) { + this._pixiObject.alpha = alphaForDisplay; + for (const layer of this._editableTileMap.getLayers()) { + // Only update layers that are of type TileMapHelper.EditableTileMapLayer. + // @ts-ignore - only this type of layer has setAlpha. + if (layer.setAlpha) { + const editableLayer = /** @type {TileMapHelper.EditableTileMapLayer} */ (layer); + editableLayer.setAlpha(alphaForDisplay); + } + } + // Only update the tilemap if the alpha has changed. + this.updatePixiTileMap(); + } } /** @@ -2148,6 +2247,9 @@ module.exports = { } } + /** + * This is called to update the PIXI object on the scene editor, without reloading the tilemap. + */ updatePixiTileMap() { const atlasImageResourceName = this._associatedObjectConfiguration .getProperties() @@ -2266,6 +2368,26 @@ module.exports = { objectToChange.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Update the opacity, if needed. + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + + if (this._editableTileMap && objectToChange.alpha !== alphaForDisplay) { + objectToChange.alpha = alphaForDisplay; + for (const layer of this._editableTileMap.getLayers()) { + // Only update layers that are of type TileMapHelper.EditableTileMapLayer. + // @ts-ignore - only this type of layer has setAlpha. + if (layer.setAlpha) { + const editableLayer = /** @type {TileMapHelper.EditableTileMapLayer} */ (layer); + editableLayer.setAlpha(alphaForDisplay); + } + } + this.updatePixiTileMap(); + } } /** diff --git a/Extensions/TileMap/simpletilemapruntimeobject.ts b/Extensions/TileMap/simpletilemapruntimeobject.ts index 128e9990d8d1..9ac1f68ceab8 100644 --- a/Extensions/TileMap/simpletilemapruntimeobject.ts +++ b/Extensions/TileMap/simpletilemapruntimeobject.ts @@ -186,11 +186,14 @@ namespace gdjs { // 2. Update the renderer so that it updates the tilemap object // (used for width and position calculations). this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => { - // 3. Set custom dimensions if applicable. + // 3. Set custom dimensions & opacity if applicable. if (initialInstanceData.customSize) { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } // 4. Update position (calculations based on renderer's dimensions). this._renderer.updatePosition(); diff --git a/Extensions/TileMap/tilemapruntimeobject.ts b/Extensions/TileMap/tilemapruntimeobject.ts index 23568ec77991..95a21f1c5571 100644 --- a/Extensions/TileMap/tilemapruntimeobject.ts +++ b/Extensions/TileMap/tilemapruntimeobject.ts @@ -203,6 +203,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } private _updateTileMap(): void { diff --git a/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts b/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts index 272e2e1804d9..01c076c9eee6 100644 --- a/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts +++ b/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts @@ -135,6 +135,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } /** diff --git a/Extensions/Video/JsExtension.js b/Extensions/Video/JsExtension.js index 83fa6fb64071..4c1ae80a484c 100644 --- a/Extensions/Video/JsExtension.js +++ b/Extensions/Video/JsExtension.js @@ -62,12 +62,6 @@ module.exports = { videoObject.getProperties = function (objectContent) { var objectProperties = new gd.MapStringPropertyDescriptor(); - objectProperties - .getOrCreate('Opacity') - .setValue(objectContent.opacity.toString()) - .setType('number') - .setLabel(_('Video opacity (0-255)')) - .setGroup(_('Appearance')); objectProperties .getOrCreate('Looped') .setValue(objectContent.loop ? 'true' : 'false') @@ -625,13 +619,6 @@ module.exports = { } } - // Update opacity - const opacity = +this._associatedObjectConfiguration - .getProperties() - .get('Opacity') - .getValue(); - this._pixiObject.alpha = opacity / 255; - // Read position and angle from the instance this._pixiObject.position.x = this._instance.getX() + this._pixiObject.width / 2; @@ -645,6 +632,13 @@ module.exports = { this._pixiObject.width = this.getCustomWidth(); this._pixiObject.height = this.getCustomHeight(); } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } /** diff --git a/Extensions/Video/videoruntimeobject.ts b/Extensions/Video/videoruntimeobject.ts index fd290e82c720..5041967b2293 100644 --- a/Extensions/Video/videoruntimeobject.ts +++ b/Extensions/Video/videoruntimeobject.ts @@ -155,6 +155,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onDestroyed(): void { diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts index 17ac9726ac31..0b3daaa8e385 100644 --- a/GDJS/Runtime/CustomRuntimeObject.ts +++ b/GDJS/Runtime/CustomRuntimeObject.ts @@ -166,6 +166,15 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } } onDeletedFromScene(parent: gdjs.RuntimeInstanceContainer): void { diff --git a/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts index ad164ac5f771..ef916467b4f7 100644 --- a/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts @@ -80,8 +80,10 @@ namespace gdjs { this._object.getY() + this._pixiContainer.pivot.y * Math.abs(scaleY); this._pixiContainer.rotation = gdjs.toRad(this._object.angle); - this._pixiContainer.scale.x = scaleX; - this._pixiContainer.scale.y = scaleY; + this._pixiContainer.scale.x = + scaleX * (this._object.isFlippedX() ? -1 : 1); + this._pixiContainer.scale.y = + scaleY * (this._object.isFlippedY() ? -1 : 1); this._pixiContainer.visible = !this._object.hidden; this._pixiContainer.alpha = opacity / 255; diff --git a/GDJS/Runtime/spriteruntimeobject.ts b/GDJS/Runtime/spriteruntimeobject.ts index 7e468982ea51..7dd4b90e9e8e 100644 --- a/GDJS/Runtime/spriteruntimeobject.ts +++ b/GDJS/Runtime/spriteruntimeobject.ts @@ -184,6 +184,15 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } } /** diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index bc607ea279bd..c6b23d620230 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -254,6 +254,11 @@ declare interface InstanceData { rotationY?: number; zOrder: number; + opacity?: number; + + flippedX?: boolean; + flippedY?: boolean; + flippedZ?: boolean; customSize: boolean; width: number; diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 4c9959739627..9e595a74e3ee 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -1309,9 +1309,18 @@ interface InitialInstance { void SetShouldKeepRatio(boolean keepRatio); long GetZOrder(); void SetZOrder(long zOrder); + long GetOpacity(); + void SetOpacity(long opacity); [Const, Ref] DOMString GetLayer(); void SetLayer([Const] DOMString layer); + boolean IsFlippedX(); + void SetFlippedX(boolean flippedX); + boolean IsFlippedY(); + void SetFlippedY(boolean flippedY); + boolean IsFlippedZ(); + void SetFlippedZ(boolean flippedZ); + void SetHasCustomSize(boolean enable); boolean HasCustomSize(); void SetHasCustomDepth(boolean enable); diff --git a/GDevelop.js/Gruntfile.js b/GDevelop.js/Gruntfile.js index 9f732359ae87..62dc69a93478 100644 --- a/GDevelop.js/Gruntfile.js +++ b/GDevelop.js/Gruntfile.js @@ -142,6 +142,8 @@ module.exports = function (grunt) { build: { src: [ buildPath, + 'Bindings/glue.cpp', + 'Bindings/glue.js', buildOutputPath + 'libGD.js', buildOutputPath + 'libGD.wasm', ], diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index bc59752084fe..d45afb572890 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -1092,8 +1092,16 @@ export class InitialInstance extends EmscriptenObject { setShouldKeepRatio(keepRatio: boolean): void; getZOrder(): number; setZOrder(zOrder: number): void; + getOpacity(): number; + setOpacity(opacity: number): void; getLayer(): string; setLayer(layer: string): void; + isFlippedX(): boolean; + setFlippedX(flippedX: boolean): void; + isFlippedY(): boolean; + setFlippedY(flippedY: boolean): void; + isFlippedZ(): boolean; + setFlippedZ(flippedZ: boolean): void; setHasCustomSize(enable: boolean): void; hasCustomSize(): boolean; setHasCustomDepth(enable: boolean): void; diff --git a/GDevelop.js/types/gdinitialinstance.js b/GDevelop.js/types/gdinitialinstance.js index 8908de31ed87..e2f540f2ca9b 100644 --- a/GDevelop.js/types/gdinitialinstance.js +++ b/GDevelop.js/types/gdinitialinstance.js @@ -23,8 +23,16 @@ declare class gdInitialInstance { setShouldKeepRatio(keepRatio: boolean): void; getZOrder(): number; setZOrder(zOrder: number): void; + getOpacity(): number; + setOpacity(opacity: number): void; getLayer(): string; setLayer(layer: string): void; + isFlippedX(): boolean; + setFlippedX(flippedX: boolean): void; + isFlippedY(): boolean; + setFlippedY(flippedY: boolean): void; + isFlippedZ(): boolean; + setFlippedZ(flippedZ: boolean): void; setHasCustomSize(enable: boolean): void; hasCustomSize(): boolean; setHasCustomDepth(enable: boolean): void; diff --git a/newIDE/app/src/CompactPropertiesEditor/index.js b/newIDE/app/src/CompactPropertiesEditor/index.js index 29e02872868b..a135da98f4ed 100644 --- a/newIDE/app/src/CompactPropertiesEditor/index.js +++ b/newIDE/app/src/CompactPropertiesEditor/index.js @@ -26,6 +26,7 @@ import VerticallyCenterWithBar from '../UI/VerticallyCenterWithBar'; import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; import { textEllipsisStyle } from '../UI/TextEllipsis'; import CompactPropertiesEditorRowField from './CompactPropertiesEditorRowField'; +import CompactToggleButtons from '../UI/CompactToggleButtons'; import { CompactToggleField } from '../UI/CompactToggleField'; import { CompactTextAreaField } from '../UI/CompactTextAreaField'; import { CompactColorField } from '../UI/CompactColorField'; @@ -47,6 +48,8 @@ export type ValueFieldCommonProperties = {| disabled?: (instances: Array) => boolean, onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array, onEditButtonClick?: () => void, + getValueFromDisplayedValue?: string => string, + getDisplayedValueFromValue?: string => string, |}; // "Primitive" value fields are "simple" fields. @@ -74,7 +77,7 @@ export type PrimitiveValueField = label: string, labelIsUserDefined?: boolean, |}>, - isHiddenWhenThereOnlyOneChoice?: boolean, + isHiddenWhenOnlyOneChoice?: boolean, getEndAdornmentIcon?: Instance => ?(className: string) => React.Node, onClickEndAdornment?: Instance => void, renderLeftIcon?: (className?: string) => React.Node, @@ -151,6 +154,18 @@ export type ActionButton = {| onClick: (instance: Instance) => void, |}; +type ToggleButtons = {| + nonFieldType: 'toggleButtons', + buttons: Array<{| + name: string, + renderIcon: (className?: string) => React.Node, + tooltip: React.Node, + getValue: Instance => boolean, + setValue: (instance: Instance, newValue: boolean) => void, + |}>, + ...ValueFieldCommonProperties, +|}; + // A value field is a primitive or a resource. export type ValueField = PrimitiveValueField | ResourceField; @@ -161,6 +176,7 @@ export type Field = | SectionTitle | Title | ActionButton + | ToggleButtons | VerticalCenterWithBar | {| name: string, @@ -433,6 +449,8 @@ const CompactPropertiesEditor = ({ hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, }); }, + getValueFromDisplayedValue: field.getValueFromDisplayedValue, + getDisplayedValueFromValue: field.getDisplayedValueFromValue, }; if (field.renderLeftIcon || field.hideLabel) { return ( @@ -692,6 +710,39 @@ const CompactPropertiesEditor = ({ [instances] ); + const renderToggleButtons = React.useCallback( + (field: ToggleButtons) => { + const buttons = field.buttons.map(button => { + // Button is toggled if all instances have a truthy value for it. + const isToggled = + instances.filter(instance => button.getValue(instance)).length === + instances.length; + return { + id: button.name, + renderIcon: button.renderIcon, + tooltip: button.tooltip, + isActive: isToggled, + onClick: () => { + instances.forEach(instance => + button.setValue(instance, !isToggled) + ); + onFieldChanged({ + instances, + hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, + }); + }, + }; + }); + + return ( + + + + ); + }, + [instances, onFieldChanged] + ); + const renderResourceField = (field: ResourceField) => { if (!project || !resourceManagementProps) { console.error( @@ -842,6 +893,8 @@ const CompactPropertiesEditor = ({ return renderSectionTitle(field); } else if (field.nonFieldType === 'button') { return renderButton(field); + } else if (field.nonFieldType === 'toggleButtons') { + return renderToggleButtons(field); } else if (field.nonFieldType === 'verticalCenterWithBar') { return renderVerticalCenterWithBar(field); } diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js index 7222789c6505..802ae7eeb7aa 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js @@ -15,6 +15,9 @@ import LetterH from '../../UI/CustomSvgIcons/LetterH'; import LetterW from '../../UI/CustomSvgIcons/LetterW'; import LetterD from '../../UI/CustomSvgIcons/LetterD'; import LetterZ from '../../UI/CustomSvgIcons/LetterZ'; +import Opacity from '../../UI/CustomSvgIcons/Opacity'; +import FlipHorizontal from '../../UI/CustomSvgIcons/FlipHorizontal'; +import FlipVertical from '../../UI/CustomSvgIcons/FlipVertical'; import Instance from '../../UI/CustomSvgIcons/Instance'; import Link from '../../UI/CustomSvgIcons/Link'; import Unlink from '../../UI/CustomSvgIcons/Unlink'; @@ -27,6 +30,7 @@ import Object2d from '../../UI/CustomSvgIcons/Object2d'; import RotateX from '../../UI/CustomSvgIcons/RotateX'; import RotateY from '../../UI/CustomSvgIcons/RotateY'; import RotateZ from '../../UI/CustomSvgIcons/RotateZ'; +import FlipZ from '../../UI/CustomSvgIcons/FlipZ'; /** * Applies ratio to value without intermediary value to avoid precision issues. @@ -414,8 +418,73 @@ const getKeepRatioField = ({ getNextValue: (currentValue: boolean) => !currentValue, }); -export const makeInstanceSchema = ({ +const getOpacityField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Opacity', + getLabel: () => i18n._(t`Opacity`), + valueType: 'number', + getValue: (instance: gdInitialInstance) => { + const opacity = instance.getOpacity(); + return Math.round((opacity / 255) * 100); + }, + setValue: (instance: gdInitialInstance, newValue: number) => { + const newOpacity = Math.round((newValue / 100) * 255); + const opacity = Math.max(0, Math.min(255, newOpacity)); + instance.setOpacity(opacity); + }, + renderLeftIcon: className => , + getDisplayedValueFromValue: (value: string): string => { + return `${value}%`; + }, + getValueFromDisplayedValue: (displayedValue: string): string => { + return displayedValue.replace('%', ''); + }, +}); + +const getFlippableButtons = ({ + i18n, + canFlipZ, +}: {| + i18n: I18nType, + canFlipZ: boolean, +|}) => ({ + name: 'Flip', + nonFieldType: 'toggleButtons', + buttons: [ + { + name: 'Flip horizontal', + renderIcon: className => , + tooltip: i18n._(t`Flip horizontally`), + getValue: (instance: gdInitialInstance): boolean => instance.isFlippedX(), + setValue: (instance: gdInitialInstance, newValue: boolean) => + instance.setFlippedX(newValue), + }, + { + name: 'Flip vertical', + tooltip: i18n._(t`Flip vertically`), + renderIcon: className => , + getValue: (instance: gdInitialInstance): boolean => instance.isFlippedY(), + setValue: (instance: gdInitialInstance, newValue: boolean) => + instance.setFlippedY(newValue), + }, + canFlipZ + ? { + name: 'Flip Z', + tooltip: i18n._(t` Flip along Z axis`), + renderIcon: className => , + getValue: (instance: gdInitialInstance): boolean => + instance.isFlippedZ(), + setValue: (instance: gdInitialInstance, newValue: boolean) => + instance.setFlippedZ(newValue), + } + : null, + ].filter(Boolean), +}); + +export const makeSchema = ({ is3DInstance, + hasOpacity, + canBeFlippedXY, + canBeFlippedZ, i18n, forceUpdate, onEditObject, @@ -423,6 +492,9 @@ export const makeInstanceSchema = ({ layersContainer, }: {| is3DInstance: boolean, + hasOpacity: boolean, + canBeFlippedXY: boolean, + canBeFlippedZ: boolean, i18n: I18nType, forceUpdate: () => void, onEditObject: (name: string) => void, @@ -500,6 +572,15 @@ export const makeInstanceSchema = ({ }, ], }, + hasOpacity + ? { + name: 'Opacity', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getOpacityField({ i18n })], + } + : null, getLayerField({ i18n, layersContainer }), { name: 'Rotation', @@ -507,10 +588,21 @@ export const makeInstanceSchema = ({ title: i18n._(t`Rotation`), preventWrap: true, removeSpacers: true, - children: getRotationXAndRotationYFields({ i18n }), + children: canBeFlippedXY + ? [getFlippableButtons({ i18n, canFlipZ: canBeFlippedZ })] + : [], }, - getRotationZField({ i18n }), - ]; + { + name: 'Rotation X and Y', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [ + ...getRotationXAndRotationYFields({ i18n }), + getRotationZField({ i18n }), + ], + }, + ].filter(Boolean); } return [ @@ -562,6 +654,15 @@ export const makeInstanceSchema = ({ }, ], }, + hasOpacity + ? { + name: 'Opacity', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getOpacityField({ i18n })], + } + : null, getLayerField({ i18n, layersContainer }), { name: 'Rotation', @@ -569,9 +670,18 @@ export const makeInstanceSchema = ({ title: i18n._(t`Rotation`), preventWrap: true, removeSpacers: true, + children: canBeFlippedXY + ? [getFlippableButtons({ i18n, canFlipZ: canBeFlippedZ })] + : [], + }, + { + name: 'Rotation Z', + type: 'row', + preventWrap: true, + removeSpacers: true, children: [getRotationZField({ i18n })], }, - ]; + ].filter(Boolean); }; export const reorderInstanceSchemaForCustomProperties = ( diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js index 78fc2ff00f5e..f20a81cd04a6 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js @@ -23,7 +23,7 @@ import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal'; import useForceUpdate from '../../Utils/UseForceUpdate'; import ErrorBoundary from '../../UI/ErrorBoundary'; import { - makeInstanceSchema, + makeSchema, reorderInstanceSchemaForCustomProperties, } from './CompactInstancePropertiesSchema'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; @@ -42,7 +42,9 @@ export const styles = { }; const noRefreshOfAllFields = () => { - console.warn("An instance tried to refresh all fields, but the editor doesn't support it."); + console.warn( + "An instance tried to refresh all fields, but the editor doesn't support it." + ); }; type Props = {| @@ -85,44 +87,6 @@ export const CompactInstancePropertiesEditor = ({ const forceUpdate = useForceUpdate(); const variablesListRef = React.useRef(null); - const schemaFor2D: Schema = React.useMemo( - () => - makeInstanceSchema({ - i18n, - is3DInstance: false, - onGetInstanceSize, - onEditObject: editObjectInPropertiesPanel, - layersContainer, - forceUpdate, - }), - [ - i18n, - onGetInstanceSize, - editObjectInPropertiesPanel, - layersContainer, - forceUpdate, - ] - ); - - const schemaFor3D: Schema = React.useMemo( - () => - makeInstanceSchema({ - i18n, - is3DInstance: true, - onGetInstanceSize, - onEditObject: editObjectInPropertiesPanel, - layersContainer, - forceUpdate, - }), - [ - i18n, - onGetInstanceSize, - editObjectInPropertiesPanel, - layersContainer, - forceUpdate, - ] - ); - const instance = instances[0]; /** * TODO: multiple instances support for variables list. Expected behavior should be: @@ -151,10 +115,20 @@ export const CompactInstancePropertiesEditor = ({ ); if (!object) return { object: undefined, instanceSchema: undefined }; - const is3DInstance = gd.MetadataProvider.getObjectMetadata( + const objectMetadata = gd.MetadataProvider.getObjectMetadata( project.getCurrentPlatform(), object.getType() - ).isRenderedIn3D(); + ); + const is3DInstance = objectMetadata.isRenderedIn3D(); + const hasOpacity = objectMetadata.hasDefaultBehavior( + 'OpacityCapability::OpacityBehavior' + ); + const canBeFlippedXY = objectMetadata.hasDefaultBehavior( + 'FlippableCapability::FlippableBehavior' + ); + const canBeFlippedZ = objectMetadata.hasDefaultBehavior( + 'Scene3D::Base3DBehavior' + ); const instanceSchemaForCustomProperties = propertiesMapToSchema( properties, (instance: gdInitialInstance) => @@ -175,11 +149,20 @@ export const CompactInstancePropertiesEditor = ({ instanceSchemaForCustomProperties, i18n ); + const instanceSchema = makeSchema({ + i18n, + is3DInstance, + hasOpacity, + canBeFlippedXY, + canBeFlippedZ, + onGetInstanceSize, + onEditObject: editObjectInPropertiesPanel, + layersContainer, + forceUpdate, + }).concat(reorderedInstanceSchemaForCustomProperties); return { object, - instanceSchema: is3DInstance - ? schemaFor3D.concat(reorderedInstanceSchemaForCustomProperties) - : schemaFor2D.concat(reorderedInstanceSchemaForCustomProperties), + instanceSchema, }; }, [ @@ -188,8 +171,10 @@ export const CompactInstancePropertiesEditor = ({ objectsContainer, project, i18n, - schemaFor3D, - schemaFor2D, + forceUpdate, + layersContainer, + onGetInstanceSize, + editObjectInPropertiesPanel, ] ); diff --git a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js index 8fd2caf061eb..e5626e418414 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js @@ -387,6 +387,7 @@ export class ChildInstance { x: number; y: number; z: number; + opacity: number; _hasCustomSize: boolean; _hasCustomDepth: boolean; _customWidth: number; @@ -397,6 +398,7 @@ export class ChildInstance { this.x = 0; this.y = 0; this.z = 0; + this.opacity = 255; this._customWidth = 0; this._customHeight = 0; this._customDepth = 0; @@ -464,6 +466,26 @@ export class ChildInstance { setZOrder(zOrder: number) {} + getOpacity() { + return this.opacity; + } + + setOpacity(opacity: number) { + this.opacity = opacity; + } + + isFlippedX() { + return false; + } + + setFlippedX(flippedX: boolean) {} + + isFlippedY() { + return false; + } + + setFlippedY(flippedY: boolean) {} + getLayer() { return ''; } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js index 8800c1d34ff1..8df0fc4edd30 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js @@ -524,6 +524,13 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance this._pixiObject.scale.x = 1; this._pixiObject.scale.y = 1; } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; + + if (this._instance.isFlippedX()) this._pixiObject.scale.x *= -1; + if (this._instance.isFlippedY()) this._pixiObject.scale.y *= -1; } getDefaultWidth() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js index 247e8787ae11..fc985e100f74 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js @@ -37,6 +37,10 @@ export default function makeRenderer(iconPath: string) { this._pixiObject.position.x = this._instance.getX(); this._pixiObject.position.y = this._instance.getY(); this._pixiObject.angle = this._instance.getAngle(); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } static getThumbnail( diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js index 9b27403f4c3e..4e17b663f054 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js @@ -87,6 +87,10 @@ export default class RenderedPanelSpriteInstance extends RenderedInstance { if (this._width !== oldWidth || this._height !== oldHeight) { this.updateWidthHeight(); } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } makeObjectsAndUpdateTextures() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js index 4b1f137f7cb6..384980bfcd6f 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js @@ -44,6 +44,10 @@ export default class RenderedParticleEmitterInstance extends RenderedInstance { update() { this._pixiObject.position.x = this._instance.getX(); this._pixiObject.position.y = this._instance.getY(); + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; + this.updateGraphics(); } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js index 2ad05e2f116e..b5c93ae5c12a 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js @@ -133,7 +133,11 @@ export default class RenderedSprite3DInstance extends Rendered3DInstance { threeObject.position.y += this._instance.getY(); threeObject.position.z += this._instance.getZ(); - threeObject.scale.set(width, height, 1); + const scaleX = width * (this._instance.isFlippedX() ? -1 : 1); + const scaleY = height * (this._instance.isFlippedY() ? -1 : 1); + const scaleZ = 1 * (this._instance.isFlippedZ() ? -1 : 1); + + threeObject.scale.set(scaleX, scaleY, scaleZ); } updateSprite(): boolean { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js index 2076f86c7cef..802cb7287f8b 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js @@ -118,6 +118,13 @@ export default class RenderedSpriteInstance extends RenderedInstance { this._pixiObject.position.y = this._instance.getY() + (this._centerY - this._originY) * Math.abs(this._pixiObject.scale.y); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; + + if (this._instance.isFlippedX()) this._pixiObject.scale.x *= -1; + if (this._instance.isFlippedY()) this._pixiObject.scale.y *= -1; } updateSprite(): boolean { @@ -235,11 +242,15 @@ export default class RenderedSpriteInstance extends RenderedInstance { getCenterX(): number { if (!this._sprite || !this._pixiObject) return 0; - return this._centerX * this._pixiObject.scale.x; // This is equivalent to `this._animationFrame.center.x * Math.abs(this._scaleX)` in the runtime. + return ( + this._centerX * Math.abs(this._pixiObject.scale.x) // This is equivalent to `this._animationFrame.center.x * Math.abs(this._scaleX)` in the runtime. + ); } getCenterY(): number { if (!this._sprite || !this._pixiObject) return 0; - return this._centerY * this._pixiObject.scale.y; // This is equivalent to `this._animationFrame.center.y * Math.abs(this._scaleY)` in the runtime. + return ( + this._centerY * Math.abs(this._pixiObject.scale.y) // This is equivalent to `this._animationFrame.center.y * Math.abs(this._scaleY)` in the runtime. + ); } } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js index 4bc14287a7ea..bf182a0e6a20 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js @@ -209,6 +209,10 @@ export default class RenderedTextInstance extends RenderedInstance { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js index de8478e8f8fc..5d1f7b0fe65e 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js @@ -89,6 +89,10 @@ export default class RenderedTiledSpriteInstance extends RenderedInstance { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { diff --git a/newIDE/app/src/Profile/ProfileDialog.js b/newIDE/app/src/Profile/ProfileDialog.js index c21d976baa6e..ff22dea9a294 100644 --- a/newIDE/app/src/Profile/ProfileDialog.js +++ b/newIDE/app/src/Profile/ProfileDialog.js @@ -32,11 +32,12 @@ type Props = {| const ProfileDialog = ({ open, onClose }: Props) => { const badgesSeenNotificationTimeoutRef = React.useRef(null); const badgesSeenNotificationSentRef = React.useRef(false); + const authenticatedUser = React.useContext(AuthenticatedUserContext); const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ includeLegacy: true, + authenticatedUser, }); - const authenticatedUser = React.useContext(AuthenticatedUserContext); const isUserLoading = authenticatedUser.loginState !== 'done'; const userAchievementsContainerRef = React.useRef(null); const markBadgesAsSeen = React.useCallback( diff --git a/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js b/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js index f25e49de162e..b84e1c745dda 100644 --- a/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js +++ b/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js @@ -57,6 +57,7 @@ export const SubscriptionSuggestionProvider = ({ const { showAlert } = useAlertDialog(); const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ includeLegacy: true, + authenticatedUser, }); const closeSubscriptionDialog = () => setAnalyticsMetadata(null); diff --git a/newIDE/app/src/PropertiesEditor/PropertiesMapToSchema.js b/newIDE/app/src/PropertiesEditor/PropertiesMapToSchema.js index d922ec8c0dcf..d36733594848 100644 --- a/newIDE/app/src/PropertiesEditor/PropertiesMapToSchema.js +++ b/newIDE/app/src/PropertiesEditor/PropertiesMapToSchema.js @@ -126,7 +126,7 @@ const createField = ( property.getExtraInfo().size() > 0 ? property.getExtraInfo().at(0) : ''; return { name, - isHiddenWhenThereOnlyOneChoice: true, + isHiddenWhenOnlyOneChoice: true, valueType: 'string', getChoices: () => { return !object || behaviorType === '' diff --git a/newIDE/app/src/PropertiesEditor/index.js b/newIDE/app/src/PropertiesEditor/index.js index 51c631666889..ffc095099c08 100644 --- a/newIDE/app/src/PropertiesEditor/index.js +++ b/newIDE/app/src/PropertiesEditor/index.js @@ -366,7 +366,7 @@ const PropertiesEditor = ({ if (!field.getChoices || !field.getValue) return; const choices = field.getChoices(); - if (choices.length < 2 && field.isHiddenWhenThereOnlyOneChoice) { + if (choices.length < 2 && field.isHiddenWhenOnlyOneChoice) { return; } diff --git a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js index f2394d766d7c..3ba736acd64a 100644 --- a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js +++ b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js @@ -30,8 +30,11 @@ const QuickBehaviorPropertiesEditor = ({ |}) => { const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute(); const basicPropertiesSchema = React.useMemo( - () => - propertiesMapToSchema( + () => { + if (schemaRecomputeTrigger) { + // schemaRecomputeTrigger allows to invalidate the schema when required. + } + return propertiesMapToSchema( behavior.getProperties(), behavior => behavior.getProperties(), (behavior, name, value) => { @@ -39,9 +42,8 @@ const QuickBehaviorPropertiesEditor = ({ }, object, 'Basic-Quick' - ), - // schemaRecomputeTrigger allows to invalidate the schema when required. - // eslint-disable-next-line react-hooks/exhaustive-deps + ); + }, [behavior, object, schemaRecomputeTrigger] ); diff --git a/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js b/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js index a3238c25da5e..8b0ad2e0113f 100644 --- a/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js +++ b/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js @@ -24,6 +24,7 @@ import { showErrorBox } from '../../UI/Messages/MessageBox'; import useForceUpdate from '../../Utils/UseForceUpdate'; import classes from './CompactResourceSelectorWithThumbnail.module.css'; import classNames from 'classnames'; +import { makeTimestampedId } from '../../Utils/TimestampedId'; const styles = { icon: { @@ -59,6 +60,7 @@ export const CompactResourceSelectorWithThumbnail = ({ const resourcesLoader = ResourcesLoader; const forceUpdate = useForceUpdate(); const displayThumbnail = resourcesKindsWithThumbnail.includes(resourceKind); + const idToUse = React.useRef(id || makeTimestampedId()); // TODO: move in a hook? const { showConfirmation } = useAlertDialog(); @@ -227,7 +229,7 @@ export const CompactResourceSelectorWithThumbnail = ({ ); return ( - + {displayThumbnail && ( {}}> + } @@ -268,8 +270,8 @@ export const CompactResourceSelectorWithThumbnail = ({ }, ...externalEditors.map(externalEditor => ({ label: resourceName - ? i18n._(externalEditor.createDisplayName) - : i18n._(externalEditor.editDisplayName), + ? i18n._(externalEditor.editDisplayName) + : i18n._(externalEditor.createDisplayName), click: () => editWith(i18n, externalEditor), })), ]} diff --git a/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js b/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js index 0de1a3217c1a..c113929759f9 100644 --- a/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js +++ b/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js @@ -48,6 +48,8 @@ type Props = {| useLeftIconAsNumberControl?: boolean, renderEndAdornmentOnHover?: (className: string) => React.Node, onClickEndAdornment?: () => void, + getValueFromDisplayedValue?: string => string, + getDisplayedValueFromValue?: string => string, errorText?: React.Node, |}; @@ -57,6 +59,8 @@ const CompactSemiControlledNumberField = ({ onChange, errorText, commitOnBlur, + getValueFromDisplayedValue, + getDisplayedValueFromValue, ...otherProps }: Props) => { const textFieldRef = React.useRef(null); @@ -137,16 +141,23 @@ const CompactSemiControlledNumberField = ({ [commitOnBlur, onChange] ); + const stringValue = getDisplayedValueFromValue + ? getDisplayedValueFromValue(value.toString()) + : value.toString(); + return (
{ setFocused(true); - setTemporaryValue(value.toString()); + const originalStringValue = getValueFromDisplayedValue + ? getValueFromDisplayedValue(stringValue) + : stringValue; + setTemporaryValue(originalStringValue); }} onKeyDown={event => { if (shouldValidate(event)) { @@ -204,7 +215,10 @@ const CompactSemiControlledNumberField = ({ } }} onBlur={event => { - if (!cancelEditionRef.current) onChangeValue(temporaryValue, 'blur'); + const newValue = getDisplayedValueFromValue + ? getDisplayedValueFromValue(temporaryValue) + : temporaryValue; + if (!cancelEditionRef.current) onChangeValue(newValue, 'blur'); setFocused(false); setTemporaryValue(''); cancelEditionRef.current = false; diff --git a/newIDE/app/src/UI/CompactTextAreaField/CompactTextField.module.css b/newIDE/app/src/UI/CompactTextAreaField/CompactTextAreaField.module.css similarity index 100% rename from newIDE/app/src/UI/CompactTextAreaField/CompactTextField.module.css rename to newIDE/app/src/UI/CompactTextAreaField/CompactTextAreaField.module.css diff --git a/newIDE/app/src/UI/CompactTextAreaField/index.js b/newIDE/app/src/UI/CompactTextAreaField/index.js index 45cf7e4bd78d..c17a20ff64af 100644 --- a/newIDE/app/src/UI/CompactTextAreaField/index.js +++ b/newIDE/app/src/UI/CompactTextAreaField/index.js @@ -2,7 +2,7 @@ import * as React from 'react'; import classNames from 'classnames'; -import classes from './CompactTextField.module.css'; +import classes from './CompactTextAreaField.module.css'; import { makeTimestampedId } from '../../Utils/TimestampedId'; import Tooltip from '@material-ui/core/Tooltip'; import Text from '../../UI/Text'; @@ -19,7 +19,7 @@ const styles = { }, }; -export type CompactTextFieldProps = {| +export type CompactTextAreaFieldProps = {| label: string, markdownDescription?: ?string, value: string, @@ -50,7 +50,7 @@ export const CompactTextAreaField = ({ disabled, errored, placeholder, -}: CompactTextFieldProps) => { +}: CompactTextAreaFieldProps) => { const idToUse = React.useRef(id || makeTimestampedId()); const title = !markdownDescription diff --git a/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css b/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css new file mode 100644 index 000000000000..7e7895b18e4a --- /dev/null +++ b/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css @@ -0,0 +1,42 @@ +.container { + display: flex; + align-items: center; + background-color: var(--theme-text-field-default-background-color); + flex: 1; +} + +.compactToggleButton { + border-radius: 4px; + color: var(--theme-text-default-color); + background-color: var(--theme-text-field-default-background-color); + transition: box-shadow 0.1s; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex: 1; + min-width: 0px; + margin: 3px; + border: 0; +} + +.separator { + background-color: var(--theme-text-field-disabled-color); + height: 15px; + width: 1px; + margin: 0 1px; +} + +/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/ +svg.icon { + font-size: 20px; + color: var(--theme-text-default-color); + transition: color 0.1s linear; +} + +.compactToggleButton.active { + background-color: var(--theme-icon-button-selected-background-color); +} +.compactToggleButton.active svg.icon { + color: var(--theme-icon-button-selected-color); +} \ No newline at end of file diff --git a/newIDE/app/src/UI/CompactToggleButtons/index.js b/newIDE/app/src/UI/CompactToggleButtons/index.js new file mode 100644 index 000000000000..a0a3ff101814 --- /dev/null +++ b/newIDE/app/src/UI/CompactToggleButtons/index.js @@ -0,0 +1,60 @@ +// @flow + +import * as React from 'react'; +import Tooltip from '@material-ui/core/Tooltip'; +import classNames from 'classnames'; +import classes from './CompactToggleButtons.module.css'; +import { tooltipEnterDelay } from '../Tooltip'; + +type CompactToggleButton = {| + id: string, + renderIcon: (className?: string) => React.Node, + tooltip: React.Node, + onClick: () => void, + isActive: boolean, +|}; +export type CompactToggleButtonsProps = {| + id: string, + buttons: CompactToggleButton[], +|}; + +const CompactToggleButtons = ({ id, buttons }: CompactToggleButtonsProps) => { + return ( +
+ {buttons.map((button, index) => ( + + + + + {index < buttons.length - 1 && ( +
+ )} + + ))} +
+ ); +}; + +export default CompactToggleButtons; diff --git a/newIDE/app/src/UI/CompactToggleField/CompactToggleField.module.css b/newIDE/app/src/UI/CompactToggleField/CompactToggleField.module.css index a76efb4b02a6..327bd8c2ed64 100644 --- a/newIDE/app/src/UI/CompactToggleField/CompactToggleField.module.css +++ b/newIDE/app/src/UI/CompactToggleField/CompactToggleField.module.css @@ -25,32 +25,57 @@ .slider { position: absolute; - cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; - background-color: #3a3a3a; + background-color: var(--theme-toggle-field-default-background-color); transition: 0.2s; border-radius: 16px; } - -.slider:before { +.handle { position: absolute; content: ''; height: 12px; width: 12px; - left: 2px; - bottom: 2px; - background-color: white; + left: 3px; + bottom: 3px; + background-color: var(--theme-toggle-field-default-slider-color); transition: 0.2s; border-radius: 50%; + z-index: 2; +} +.handleContainer { + position: absolute; + content: ''; + height: 18px; + width: 18px; + left: -1px; + bottom: -1px; + background-color: none; + border-radius: 50%; + z-index: 1; + transition: 0.2s; } -.slider.checked { - background-color: #4c2f8c; +.slider:hover .handleContainer:not(.disabled) { + background-color: var(--theme-toggle-field-hover-slider-aura-color); } -.slider.checked:before { +.handleContainer.checked { transform: translateX(16px); } + +.slider.checked { + background-color: var(--theme-toggle-field-active-background-color); +} +.handle.checked { + background-color: var(--theme-toggle-field-active-slider-color); +} + +.slider.disabled { + background-color: var(--theme-toggle-field-disabled-background-color); +} +.handle.disabled { + background-color: var(--theme-toggle-field-disabled-slider-color); +} diff --git a/newIDE/app/src/UI/CompactToggleField/index.js b/newIDE/app/src/UI/CompactToggleField/index.js index bd4c9956ae00..8860dbd1fd29 100644 --- a/newIDE/app/src/UI/CompactToggleField/index.js +++ b/newIDE/app/src/UI/CompactToggleField/index.js @@ -43,13 +43,31 @@ export const CompactToggleField = (props: Props) => { type="checkbox" class={classes.checkbox} onChange={() => props.onCheck(!props.checked)} + disabled={props.disabled} /> + > + + + +
( + + + + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/Opacity.js b/newIDE/app/src/UI/CustomSvgIcons/Opacity.js new file mode 100644 index 000000000000..bb850d015301 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/Opacity.js @@ -0,0 +1,41 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json b/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json index 9f2960a50ffc..7bea02ffc5af 100644 --- a/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json @@ -497,6 +497,42 @@ "value": "rgba(221, 209, 255, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#7F7F85", + "comment": "Palette/Grey/50" + } + }, + "active": { + "slider-color": { + "value": "#008DFF" + }, + "background-color": { + "value": "#006DEE" + } + }, + "disabled": { + "slider-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "background-color": { + "value": "#32323b", + "comment": "Palette/Grey/80" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(217, 217, 222, 0.30)", + "comment": "Palette/Grey/20 and alpha" + } + } } }, "input": { @@ -746,4 +782,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json b/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json index 308899e5ef13..607ee5123af6 100644 --- a/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json @@ -573,6 +573,44 @@ "value": "rgba(221, 209, 255, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#606166", + "comment": "Palette/Grey/60" + } + }, + "active": { + "slider-color": { + "value": "#5028cc", + "comment": "Palette/Purple/50" + }, + "background-color": { + "value": "#9979f1", + "comment": "Palette/Purple/30" + } + }, + "disabled": { + "slider-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "background-color": { + "value": "#32323b", + "comment": "Palette/Grey/80" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(217, 217, 222, 0.30)", + "comment": "Palette/Grey/20 and alpha" + } + } } }, "input": { @@ -824,4 +862,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json b/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json index df7224f04378..762b3a55ebe1 100644 --- a/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json @@ -561,6 +561,44 @@ "value": "rgba(79, 40, 205, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#606166", + "comment": "Palette/Grey/60" + } + }, + "active": { + "slider-color": { + "value": "#5028cc", + "comment": "Palette/Purple/50" + }, + "background-color": { + "value": "#9979f1", + "comment": "Palette/Purple/30" + } + }, + "disabled": { + "slider-color": { + "value": "#A6A6AB", + "comment": "Palette/Grey/40" + }, + "background-color": { + "value": "#7F7F85", + "comment": "Palette/Grey/50" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(197, 197, 201, 0.30)", + "comment": "Palette/Grey/30 and alpha" + } + } } }, "input": { diff --git a/newIDE/app/src/UI/Theme/NordTheme/theme.json b/newIDE/app/src/UI/Theme/NordTheme/theme.json index b8f03e40b995..1617196dc563 100644 --- a/newIDE/app/src/UI/Theme/NordTheme/theme.json +++ b/newIDE/app/src/UI/Theme/NordTheme/theme.json @@ -488,6 +488,42 @@ "value": "rgba(221, 209, 255, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#7F7F85", + "comment": "Palette/Grey/50" + } + }, + "active": { + "slider-color": { + "value": "#7EA1CC" + }, + "background-color": { + "value": "#5E81AC" + } + }, + "disabled": { + "slider-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "background-color": { + "value": "#32323b", + "comment": "Palette/Grey/80" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(217, 217, 222, 0.30)", + "comment": "Palette/Grey/20 and alpha" + } + } } }, "input": { diff --git a/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json b/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json index ce5b54c6c145..29b448001607 100644 --- a/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json @@ -503,6 +503,42 @@ "value": "rgba(221, 209, 255, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#7F7F85", + "comment": "Palette/Grey/50" + } + }, + "active": { + "slider-color": { + "value": "#81CFFF" + }, + "background-color": { + "value": "#61AFEF" + } + }, + "disabled": { + "slider-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "background-color": { + "value": "#32323b", + "comment": "Palette/Grey/80" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(217, 217, 222, 0.30)", + "comment": "Palette/Grey/20 and alpha" + } + } } }, "input": { diff --git a/newIDE/app/src/UI/Theme/RosePineTheme/theme.json b/newIDE/app/src/UI/Theme/RosePineTheme/theme.json index c7c6a031098f..171acc22239b 100644 --- a/newIDE/app/src/UI/Theme/RosePineTheme/theme.json +++ b/newIDE/app/src/UI/Theme/RosePineTheme/theme.json @@ -489,6 +489,42 @@ "value": "rgba(221, 209, 255, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#7F7F85", + "comment": "Palette/Grey/50" + } + }, + "active": { + "slider-color": { + "value": "#E4C7FF" + }, + "background-color": { + "value": "#C4A7E7" + } + }, + "disabled": { + "slider-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "background-color": { + "value": "#32323b", + "comment": "Palette/Grey/80" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(217, 217, 222, 0.30)", + "comment": "Palette/Grey/20 and alpha" + } + } } }, "input": { diff --git a/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json b/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json index ea9822ba238a..40136df65d2d 100644 --- a/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json @@ -490,6 +490,42 @@ "value": "rgba(221, 209, 255, 0.16)" } } + }, + "toggle-field": { + "default": { + "slider-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "background-color": { + "value": "#7F7F85", + "comment": "Palette/Grey/50" + } + }, + "active": { + "slider-color": { + "value": "#249AC6" + }, + "background-color": { + "value": "#047AA6" + } + }, + "disabled": { + "slider-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "background-color": { + "value": "#32323b", + "comment": "Palette/Grey/80" + } + }, + "hover": { + "slider-aura-color": { + "value": "rgba(217, 217, 222, 0.30)", + "comment": "Palette/Grey/20 and alpha" + } + } } }, "input": { diff --git a/newIDE/app/src/Utils/GDevelopServices/Usage.js b/newIDE/app/src/Utils/GDevelopServices/Usage.js index 18ab7fcc8d0e..b0454aa9524b 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Usage.js +++ b/newIDE/app/src/Utils/GDevelopServices/Usage.js @@ -221,7 +221,23 @@ export const canPriceBeFoundInGDevelopPrices = ( export const listSubscriptionPlans = async (options: {| includeLegacy: boolean, + getAuthorizationHeader?: ?() => Promise, + userId?: ?string, |}): Promise => { + if (options.userId && options.getAuthorizationHeader) { + const authorizationHeader = await options.getAuthorizationHeader(); + + const response = await apiClient.get('/subscription-plan', { + params: { + includeLegacy: options.includeLegacy ? 'true' : 'false', + userId: options.userId, + }, + headers: { + Authorization: authorizationHeader, + }, + }); + return response.data; + } const response = await apiClient.get('/subscription-plan', { params: { includeLegacy: options.includeLegacy ? 'true' : 'false' }, }); @@ -248,13 +264,31 @@ export const getSubscriptionPlanPricingSystem = async ( export const listSubscriptionPlanPricingSystems = async (options: {| subscriptionPlanIds?: ?(string[]), includeLegacy: boolean, + getAuthorizationHeader?: ?() => Promise, + userId?: ?string, |}): Promise => { - const params: {| includeLegacy: string, subscriptionPlanIds?: string |} = { + const params: {| + includeLegacy: string, + subscriptionPlanIds?: string, + userId?: string, + |} = { includeLegacy: options.includeLegacy ? 'true' : 'false', }; if (options.subscriptionPlanIds && options.subscriptionPlanIds.length > 0) { params.subscriptionPlanIds = options.subscriptionPlanIds.join(','); } + if (options.userId && options.getAuthorizationHeader) { + params.userId = options.userId; + const authorizationHeader = await options.getAuthorizationHeader(); + const response = await apiClient.get('/subscription-plan-pricing-system', { + params, + headers: { + Authorization: authorizationHeader, + }, + }); + return response.data; + } + const response = await apiClient.get('/subscription-plan-pricing-system', { params, }); diff --git a/newIDE/app/src/Utils/UseForceUpdate.js b/newIDE/app/src/Utils/UseForceUpdate.js index dac760f33f41..727fc1cec6ce 100644 --- a/newIDE/app/src/Utils/UseForceUpdate.js +++ b/newIDE/app/src/Utils/UseForceUpdate.js @@ -10,7 +10,7 @@ export default function useForceUpdate() { } export function useForceRecompute() { - const [recomputeTrigger, updateState] = React.useState(); + const [recomputeTrigger, updateState] = React.useState({}); const forceRecompute = React.useCallback(() => updateState({}), []); return [recomputeTrigger, forceRecompute]; diff --git a/newIDE/app/src/Utils/UseSubscriptionPlans.js b/newIDE/app/src/Utils/UseSubscriptionPlans.js index 7bbdc13e8220..39b520b9026d 100644 --- a/newIDE/app/src/Utils/UseSubscriptionPlans.js +++ b/newIDE/app/src/Utils/UseSubscriptionPlans.js @@ -8,6 +8,7 @@ import { type SubscriptionPlan, type SubscriptionPlanPricingSystem, } from './GDevelopServices/Usage'; +import { type AuthenticatedUser } from '../Profile/AuthenticatedUserContext'; const mergeSubscriptionPlansWithPrices = ( subscriptionPlans: SubscriptionPlan[], @@ -51,28 +52,46 @@ export const getAvailableSubscriptionPlansWithPrices = ( return availableSubscriptionPlansWithPrices; }; -type Props = {| includeLegacy: boolean |}; +type Props = {| + includeLegacy: boolean, + authenticatedUser?: AuthenticatedUser, +|}; /** * Hook to access subscription plans across the app. */ -const useSubscriptionPlans = ({ includeLegacy }: Props) => { +const useSubscriptionPlans = ({ includeLegacy, authenticatedUser }: Props) => { const [ subscriptionPlansWithPricingSystems, setSubscriptionPlansWithPrices, ] = React.useState(null); + const userId = + authenticatedUser && authenticatedUser.profile + ? authenticatedUser.profile.id + : null; + const getAuthorizationHeader = authenticatedUser + ? authenticatedUser.getAuthorizationHeader + : null; const fetchSubscriptionPlansAndPrices = React.useCallback( async () => { const results = await Promise.all([ - listSubscriptionPlans({ includeLegacy }), - listSubscriptionPlanPricingSystems({ includeLegacy }), + listSubscriptionPlans({ + includeLegacy, + getAuthorizationHeader, + userId, + }), + listSubscriptionPlanPricingSystems({ + includeLegacy, + getAuthorizationHeader, + userId, + }), ]); setSubscriptionPlansWithPrices( mergeSubscriptionPlansWithPrices(results[0], results[1]) ); }, - [includeLegacy] + [includeLegacy, getAuthorizationHeader, userId] ); React.useEffect( diff --git a/newIDE/app/src/stories/componentStories/ResourcesList/CompactResourceSelectorWithThumbnail.stories.js b/newIDE/app/src/stories/componentStories/ResourcesList/CompactResourceSelectorWithThumbnail.stories.js new file mode 100644 index 000000000000..02cdd81acb17 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/ResourcesList/CompactResourceSelectorWithThumbnail.stories.js @@ -0,0 +1,123 @@ +// @flow +import * as React from 'react'; + +import muiDecorator from '../../ThemeDecorator'; +import paperDecorator from '../../PaperDecorator'; + +import { action } from '@storybook/addon-actions'; +import { testProject } from '../../GDevelopJsInitializerDecorator'; +import { CompactResourceSelectorWithThumbnail } from '../../../ResourcesList/CompactResourceSelectorWithThumbnail'; +import { ColumnStackLayout } from '../../../UI/Layout'; +import ElementHighlighterProvider from '../../ElementHighlighterProvider'; +import Text from '../../../UI/Text'; +import fakeResourceManagementProps from '../../FakeResourceManagement'; +import { emptyStorageProvider } from '../../../ProjectsStorage/ProjectStorageProviders'; +import fakeResourceExternalEditors from '../../FakeResourceExternalEditors'; + +export default { + title: 'UI Building Blocks/CompactResourceSelectorWithThumbnail', + component: CompactResourceSelectorWithThumbnail, + decorators: [paperDecorator, muiDecorator], +}; + +export const Default = () => { + return ( + + + No image selected + + Image selected + + With label + + With description + + With multiple external editors + emptyStorageProvider, + onFetchNewlyAddedResources: async () => {}, + resourceSources: [], + onChooseResource: () => Promise.reject('Unimplemented'), + resourceExternalEditors: [ + ...fakeResourceExternalEditors, + { + name: 'fake-image-editor-2', + createDisplayName: 'Create with Super Image Editor 2', + editDisplayName: 'Edit with Super Image Editor 2', + kind: 'image', + edit: async options => { + console.log( + 'Open the image editor with these options:', + options + ); + return null; + }, + }, + ], + getStorageProviderResourceOperations: () => null, + canInstallPrivateAsset: () => false, + }} + resourceName="icon128.png" + onChange={action('on change')} + /> + Not existing + + Audio + + Font + + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/UI/CompactColorField.stories.js b/newIDE/app/src/stories/componentStories/UI/CompactColorField.stories.js new file mode 100644 index 000000000000..acdacbf3af31 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/UI/CompactColorField.stories.js @@ -0,0 +1,62 @@ +// @flow +import * as React from 'react'; + +import muiDecorator from '../../ThemeDecorator'; +import paperDecorator from '../../PaperDecorator'; + +import { CompactColorField } from '../../../UI/CompactColorField'; +import { ColumnStackLayout } from '../../../UI/Layout'; +import ElementHighlighterProvider from '../../ElementHighlighterProvider'; +import Text from '../../../UI/Text'; + +export default { + title: 'UI Building Blocks/CompactColorField', + component: CompactColorField, + decorators: [paperDecorator, muiDecorator], +}; + +export const Default = () => { + const [value, setValue] = React.useState('00;00;255'); + const [value2, setValue2] = React.useState('00;255;00'); + const [value3, setValue3] = React.useState('255;00;00'); + const [value4, setValue4] = React.useState('255;255;00'); + return ( + + + Default + + Disabled + + With placeholder + + errored + + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/UI/CompactSelectField.stories.js b/newIDE/app/src/stories/componentStories/UI/CompactSelectField.stories.js index f2534a27d2c9..f3798ce8daf0 100644 --- a/newIDE/app/src/stories/componentStories/UI/CompactSelectField.stories.js +++ b/newIDE/app/src/stories/componentStories/UI/CompactSelectField.stories.js @@ -8,6 +8,7 @@ import CompactSelectField from '../../../UI/CompactSelectField'; import { ColumnStackLayout } from '../../../UI/Layout'; import Layers from '../../../UI/CustomSvgIcons/Layers'; import ElementHighlighterProvider from '../../ElementHighlighterProvider'; +import Text from '../../../UI/Text'; export default { title: 'UI Building Blocks/CompactSelectField', @@ -36,18 +37,22 @@ export const Default = () => { ]} > + Without icon {options} + Errored {options} + With empty option {[ , ...options, ]} + Disabled { > {options} + With icon { > {options} + errored { > {options} + With empty option { ...options, ]} + Disabled { + const [value, setValue] = React.useState(''); + const [value2, setValue2] = React.useState(''); + const [value3, setValue3] = React.useState(''); + const [value4, setValue4] = React.useState(''); + const [value5, setValue5] = React.useState(''); + return ( + + + + + + + + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/UI/CompactToggleButtons.stories.js b/newIDE/app/src/stories/componentStories/UI/CompactToggleButtons.stories.js new file mode 100644 index 000000000000..8682b7b6cfb0 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/UI/CompactToggleButtons.stories.js @@ -0,0 +1,110 @@ +// @flow +import * as React from 'react'; + +import muiDecorator from '../../ThemeDecorator'; +import paperDecorator from '../../PaperDecorator'; + +import CompactToggleButtons from '../../../UI/CompactToggleButtons'; +import { ColumnStackLayout } from '../../../UI/Layout'; +import Layers from '../../../UI/CustomSvgIcons/Layers'; +import ElementHighlighterProvider from '../../ElementHighlighterProvider'; +import Text from '../../../UI/Text'; + +export default { + title: 'UI Building Blocks/CompactToggleButtons', + component: CompactToggleButtons, + decorators: [paperDecorator, muiDecorator], +}; + +export const Default = () => { + const [value, setValue] = React.useState(false); + const [value1, setValue1] = React.useState(true); + const [value2, setValue2] = React.useState(false); + const [value3, setValue3] = React.useState(true); + const [value4, setValue4] = React.useState(false); + const [value5, setValue5] = React.useState(false); + return ( + + + One item + , + tooltip: 'Layer', + onClick: () => { + setValue(!value); + }, + isActive: value, + }, + ]} + /> + Two items + , + tooltip: 'Layer', + onClick: () => { + setValue1(!value1); + }, + isActive: value1, + }, + { + id: 'button2', + renderIcon: className => , + tooltip: 'Layer', + onClick: () => { + setValue2(!value2); + }, + isActive: value2, + }, + ]} + /> + Three items + , + tooltip: 'Layer', + onClick: () => { + setValue3(!value3); + }, + isActive: value3, + }, + { + id: 'button2', + renderIcon: className => , + tooltip: 'Layer', + onClick: () => { + setValue4(!value4); + }, + isActive: value4, + }, + { + id: 'button3', + renderIcon: className => , + tooltip: 'Layer', + onClick: () => { + setValue5(!value5); + }, + isActive: value5, + }, + ]} + /> + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/UI/CompactToggleField.stories.js b/newIDE/app/src/stories/componentStories/UI/CompactToggleField.stories.js new file mode 100644 index 000000000000..e13ccb0605b6 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/UI/CompactToggleField.stories.js @@ -0,0 +1,47 @@ +// @flow +import * as React from 'react'; + +import muiDecorator from '../../ThemeDecorator'; +import paperDecorator from '../../PaperDecorator'; + +import { CompactToggleField } from '../../../UI/CompactToggleField'; +import { ColumnStackLayout } from '../../../UI/Layout'; +import ElementHighlighterProvider from '../../ElementHighlighterProvider'; + +export default { + title: 'UI Building Blocks/CompactToggleField', + component: CompactToggleField, + decorators: [paperDecorator, muiDecorator], +}; + +export const Default = () => { + const [value, setValue] = React.useState(false); + const [value2, setValue2] = React.useState(true); + const [value3, setValue3] = React.useState(false); + return ( + + + + + + + + ); +};