From 364ec2ecfbe80bdbff3f6a2fbab8385fa8cf535c Mon Sep 17 00:00:00 2001 From: AlexandreS <32449369+AlexandreSi@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:21:37 +0200 Subject: [PATCH 1/3] Fix crashes in Preview due to ill-formed color values (#6980) - Make color parsing from string more robust (issues when setting colors in Sprite, BBText, Particle emitter and effects with colors) - Allow use of hex strings and shorthand hex strings in color fields - Remove UI glitch when switching effect type and both effects have parameters with identical names --- Extensions/3D/AmbientLight.ts | 4 +- Extensions/3D/DirectionalLight.ts | 2 +- Extensions/3D/ExponentialFog.ts | 2 +- Extensions/3D/HemisphereLight.ts | 4 +- Extensions/3D/LinearFog.ts | 2 +- Extensions/Effects/bevel-pixi-filter.ts | 8 +-- .../Effects/color-replace-pixi-filter.ts | 8 +-- Extensions/Effects/drop-shadow-pixi-filter.ts | 4 +- Extensions/Effects/glow-pixi-filter.ts | 2 +- Extensions/Effects/outline-pixi-filter.ts | 4 +- .../ParticleSystem/particleemitterobject.ts | 22 +++--- GDJS/Runtime/gd.ts | 71 ++++++++++++++++--- .../pixi-renderers/pixi-filters-tools.ts | 16 ----- .../spriteruntimeobject-pixi-renderer.ts | 12 +--- GDJS/Runtime/spriteruntimeobject.ts | 6 +- GDJS/tests/tests/colors.js | 55 ++++++++++++++ newIDE/app/src/EffectsList/index.js | 1 + 17 files changed, 142 insertions(+), 81 deletions(-) create mode 100644 GDJS/tests/tests/colors.js diff --git a/Extensions/3D/AmbientLight.ts b/Extensions/3D/AmbientLight.ts index 3efbb8621d50..438ff6761641 100644 --- a/Extensions/3D/AmbientLight.ts +++ b/Extensions/3D/AmbientLight.ts @@ -73,9 +73,7 @@ namespace gdjs { } updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { - this.light.color.setHex( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) - ); + this.light.color.setHex(gdjs.rgbOrHexStringToNumber(value)); } } updateColorParameter(parameterName: string, value: number): void { diff --git a/Extensions/3D/DirectionalLight.ts b/Extensions/3D/DirectionalLight.ts index 8b8d60599b8e..e481412120dd 100644 --- a/Extensions/3D/DirectionalLight.ts +++ b/Extensions/3D/DirectionalLight.ts @@ -94,7 +94,7 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { this.light.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } if (parameterName === 'top') { diff --git a/Extensions/3D/ExponentialFog.ts b/Extensions/3D/ExponentialFog.ts index 976cd9a70a45..dc39b12e0f19 100644 --- a/Extensions/3D/ExponentialFog.ts +++ b/Extensions/3D/ExponentialFog.ts @@ -71,7 +71,7 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { this.fog.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } } diff --git a/Extensions/3D/HemisphereLight.ts b/Extensions/3D/HemisphereLight.ts index 288042205ba8..ed803501d4c8 100644 --- a/Extensions/3D/HemisphereLight.ts +++ b/Extensions/3D/HemisphereLight.ts @@ -95,12 +95,12 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'skyColor') { this.light.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } if (parameterName === 'groundColor') { this.light.groundColor = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } if (parameterName === 'top') { diff --git a/Extensions/3D/LinearFog.ts b/Extensions/3D/LinearFog.ts index b2381a76b95f..bf821fe05e31 100644 --- a/Extensions/3D/LinearFog.ts +++ b/Extensions/3D/LinearFog.ts @@ -76,7 +76,7 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { this.fog.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } } diff --git a/Extensions/Effects/bevel-pixi-filter.ts b/Extensions/Effects/bevel-pixi-filter.ts index 8148082fad6b..f9df16eba7c9 100644 --- a/Extensions/Effects/bevel-pixi-filter.ts +++ b/Extensions/Effects/bevel-pixi-filter.ts @@ -67,14 +67,10 @@ namespace gdjs { const bevelFilter = (filter as unknown) as PIXI.filters.BevelFilter & BevelFilterExtra; if (parameterName === 'lightColor') { - bevelFilter.lightColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + bevelFilter.lightColor = gdjs.rgbOrHexStringToNumber(value); } if (parameterName === 'shadowColor') { - bevelFilter.shadowColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + bevelFilter.shadowColor = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/color-replace-pixi-filter.ts b/Extensions/Effects/color-replace-pixi-filter.ts index 0d004f467cc8..eb75a8e5e568 100644 --- a/Extensions/Effects/color-replace-pixi-filter.ts +++ b/Extensions/Effects/color-replace-pixi-filter.ts @@ -45,13 +45,9 @@ namespace gdjs { const colorReplaceFilter = (filter as unknown) as PIXI.filters.ColorReplaceFilter & ColorReplaceFilterExtra; if (parameterName === 'originalColor') { - colorReplaceFilter.originalColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + colorReplaceFilter.originalColor = gdjs.rgbOrHexStringToNumber(value); } else if (parameterName === 'newColor') { - colorReplaceFilter.newColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + colorReplaceFilter.newColor = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/drop-shadow-pixi-filter.ts b/Extensions/Effects/drop-shadow-pixi-filter.ts index c9c163fe3d73..46c878e976c9 100644 --- a/Extensions/Effects/drop-shadow-pixi-filter.ts +++ b/Extensions/Effects/drop-shadow-pixi-filter.ts @@ -66,9 +66,7 @@ namespace gdjs { ) { const dropShadowFilter = (filter as unknown) as PIXI.filters.DropShadowFilter; if (parameterName === 'color') { - dropShadowFilter.color = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + dropShadowFilter.color = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/glow-pixi-filter.ts b/Extensions/Effects/glow-pixi-filter.ts index 49c2419a0906..fbc16eea0ac5 100644 --- a/Extensions/Effects/glow-pixi-filter.ts +++ b/Extensions/Effects/glow-pixi-filter.ts @@ -53,7 +53,7 @@ namespace gdjs { const glowFilter = (filter as unknown) as PIXI.filters.GlowFilter & GlowFilterExtra; if (parameterName === 'color') { - glowFilter.color = gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value); + glowFilter.color = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/outline-pixi-filter.ts b/Extensions/Effects/outline-pixi-filter.ts index e1c1a037aa2c..0dce37f67b9f 100644 --- a/Extensions/Effects/outline-pixi-filter.ts +++ b/Extensions/Effects/outline-pixi-filter.ts @@ -41,9 +41,7 @@ namespace gdjs { ) { const outlineFilter = (filter as unknown) as PIXI.filters.OutlineFilter; if (parameterName === 'color') { - outlineFilter.color = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + outlineFilter.color = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/ParticleSystem/particleemitterobject.ts b/Extensions/ParticleSystem/particleemitterobject.ts index 09bb9fce357c..83b953547ec8 100644 --- a/Extensions/ParticleSystem/particleemitterobject.ts +++ b/Extensions/ParticleSystem/particleemitterobject.ts @@ -922,23 +922,17 @@ namespace gdjs { } setParticleColor1(rgbColor: string): void { - const colors = rgbColor.split(';'); - if (colors.length < 3) { - return; - } - this.setParticleRed1(parseInt(colors[0], 10)); - this.setParticleGreen1(parseInt(colors[1], 10)); - this.setParticleBlue1(parseInt(colors[2], 10)); + const colors = gdjs.rgbOrHexToRGBColor(rgbColor); + this.setParticleRed1(colors[0]); + this.setParticleGreen1(colors[1]); + this.setParticleBlue1(colors[2]); } setParticleColor2(rgbColor: string): void { - const colors = rgbColor.split(';'); - if (colors.length < 3) { - return; - } - this.setParticleRed2(parseInt(colors[0], 10)); - this.setParticleGreen2(parseInt(colors[1], 10)); - this.setParticleBlue2(parseInt(colors[2], 10)); + const colors = gdjs.rgbOrHexToRGBColor(rgbColor); + this.setParticleRed2(colors[0]); + this.setParticleGreen2(colors[1]); + this.setParticleBlue2(colors[2]); } getParticleSize1(): float { diff --git a/GDJS/Runtime/gd.ts b/GDJS/Runtime/gd.ts index fd2bd17905db..82e3ded9f09c 100644 --- a/GDJS/Runtime/gd.ts +++ b/GDJS/Runtime/gd.ts @@ -10,6 +10,9 @@ */ namespace gdjs { const logger = new gdjs.Logger('Engine runtime'); + const hexStringRegex = /^(#{0,1}[A-Fa-f0-9]{6})$/; + const shorthandHexStringRegex = /^(#{0,1}[A-Fa-f0-9]{3})$/; + const rgbStringRegex = /^(\d{1,3};\d{1,3};\d{1,3})/; /** * Contains functions used by events (this is a convention only, functions can actually @@ -61,7 +64,7 @@ namespace gdjs { }; /** - * Convert a Hex string to an RGB color array [r, g, b], where each component is in the range [0, 255]. + * Convert a Hex string (#124FE4) to an RGB color array [r, g, b], where each component is in the range [0, 255]. * * @param {string} hex Color hexadecimal */ @@ -74,6 +77,24 @@ namespace gdjs { : [0, 0, 0]; }; + /** + * Convert a shorthand Hex string (#1F4) to an RGB color array [r, g, b], where each component is in the range [0, 255]. + * + * @param {string} hex Color hexadecimal + */ + export const shorthandHexToRGBColor = function ( + hexString: string + ): [number, number, number] { + const hexNumber = parseInt(hexString.replace('#', ''), 16); + return Number.isFinite(hexNumber) + ? [ + 17 * ((hexNumber >> 8) & 0xf), + 17 * ((hexNumber >> 4) & 0xf), + 17 * (hexNumber & 0xf), + ] + : [0, 0, 0]; + }; + /** * Convert a RGB string ("rrr;ggg;bbb") or a Hex string ("#rrggbb") to a RGB color array ([r,g,b] with each component going from 0 to 255). * @param value The color as a RGB string or Hex string @@ -81,17 +102,28 @@ namespace gdjs { export const rgbOrHexToRGBColor = function ( value: string ): [number, number, number] { - const splitValue = value.split(';'); - // If a RGB string is provided, return the RGB object. - if (splitValue.length === 3) { - return [ - parseInt(splitValue[0], 10), - parseInt(splitValue[1], 10), - parseInt(splitValue[2], 10), - ]; + const rgbColor = extractRGBString(value); + if (rgbColor) { + const splitValue = rgbColor.split(';'); + // If a RGB string is provided, return the RGB object. + if (splitValue.length === 3) { + return [ + Math.min(255, parseInt(splitValue[0], 10)), + Math.min(255, parseInt(splitValue[1], 10)), + Math.min(255, parseInt(splitValue[2], 10)), + ]; + } + } + + const hexColor = extractHexString(value); + if (hexColor) { + return hexToRGBColor(hexColor); + } + const shorthandHexColor = extractShorthandHexString(value); + if (shorthandHexColor) { + return shorthandHexToRGBColor(shorthandHexColor); } - // Otherwise, convert the Hex to RGB. - return hexToRGBColor(value); + return [0, 0, 0]; }; /** @@ -146,6 +178,23 @@ namespace gdjs { ]; }; + export const extractHexString = (str: string): string | null => { + const matches = str.match(hexStringRegex); + if (!matches) return null; + return matches[0]; + }; + export const extractShorthandHexString = (str: string): string | null => { + const matches = str.match(shorthandHexStringRegex); + if (!matches) return null; + return matches[0]; + }; + + export const extractRGBString = (str: string): string | null => { + const matches = str.match(rgbStringRegex); + if (!matches) return null; + return matches[0]; + }; + /** * Get a random integer between 0 and max. * @param max The maximum value (inclusive). diff --git a/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts b/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts index 0642b23c6c10..c4f12b12a18a 100644 --- a/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts +++ b/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts @@ -52,22 +52,6 @@ namespace gdjs { _filterCreators[filterName] = filterCreator; }; - /** - * Convert a string RGB color ("rrr;ggg;bbb") or a hex string (#rrggbb) to a hex number. - * @param value The color as a RGB string or hex string - */ - export const rgbOrHexToHexNumber = function (value: string): number { - const splitValue = value.split(';'); - if (splitValue.length === 3) { - return gdjs.rgbToHexNumber( - parseInt(splitValue[0], 10), - parseInt(splitValue[1], 10), - parseInt(splitValue[2], 10) - ); - } - return parseInt(value.replace('#', '0x'), 16); - }; - /** A wrapper allowing to create an effect. */ export interface FilterCreator { /** Function to call to create the filter */ diff --git a/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts index 030d3dba6882..d676323b9c6c 100644 --- a/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts @@ -142,16 +142,8 @@ namespace gdjs { this._sprite.visible = !this._object.hidden; } - setColor(rgbColor): void { - const colors = rgbColor.split(';'); - if (colors.length < 3) { - return; - } - this._sprite.tint = gdjs.rgbToHexNumber( - parseInt(colors[0], 10), - parseInt(colors[1], 10), - parseInt(colors[2], 10) - ); + setColor(rgbOrHexColor): void { + this._sprite.tint = gdjs.rgbOrHexStringToNumber(rgbOrHexColor); } getColor() { diff --git a/GDJS/Runtime/spriteruntimeobject.ts b/GDJS/Runtime/spriteruntimeobject.ts index 7dd4b90e9e8e..4b9227fc35bb 100644 --- a/GDJS/Runtime/spriteruntimeobject.ts +++ b/GDJS/Runtime/spriteruntimeobject.ts @@ -759,10 +759,10 @@ namespace gdjs { /** * Change the tint of the sprite object. * - * @param rgbColor The color, in RGB format ("128;200;255"). + * @param rgbOrHexColor The color as a string, in RGB format ("128;200;255") or Hex format. */ - setColor(rgbColor: string): void { - this._renderer.setColor(rgbColor); + setColor(rgbOrHexColor: string): void { + this._renderer.setColor(rgbOrHexColor); } /** diff --git a/GDJS/tests/tests/colors.js b/GDJS/tests/tests/colors.js new file mode 100644 index 000000000000..01bcc50f2f65 --- /dev/null +++ b/GDJS/tests/tests/colors.js @@ -0,0 +1,55 @@ +describe('gdjs', function () { + it('should define gdjs', function () { + expect(gdjs).to.be.ok(); + }); + + describe('Color conversion', function () { + describe('Hex strings to RGB components', () => { + it('should convert hex strings', function () { + expect(gdjs.hexToRGBColor('#FFFFfF')).to.eql([255, 255, 255]); + expect(gdjs.hexToRGBColor('#000000')).to.eql([0, 0, 0]); + expect(gdjs.hexToRGBColor('#1245F5')).to.eql([18, 69, 245]); + }); + it('should convert hex strings without hashtag', function () { + expect(gdjs.hexToRGBColor('FFFFfF')).to.eql([255, 255, 255]); + expect(gdjs.hexToRGBColor('000000')).to.eql([0, 0, 0]); + expect(gdjs.hexToRGBColor('1245F5')).to.eql([18, 69, 245]); + }); + it('should convert shorthand hex strings', function () { + expect(gdjs.shorthandHexToRGBColor('#FfF')).to.eql([255, 255, 255]); + expect(gdjs.shorthandHexToRGBColor('#000')).to.eql([0, 0, 0]); + expect(gdjs.shorthandHexToRGBColor('#F3a')).to.eql([255, 51, 170]); + }); + it('should convert shorthand hex strings without hashtag', function () { + expect(gdjs.shorthandHexToRGBColor('FFF')).to.eql([255, 255, 255]); + expect(gdjs.shorthandHexToRGBColor('000')).to.eql([0, 0, 0]); + expect(gdjs.shorthandHexToRGBColor('F3a')).to.eql([255, 51, 170]); + }); + }); + describe.only('RGB strings to RGB components', () => { + it('should convert rgb strings', function () { + expect(gdjs.rgbOrHexToRGBColor('0;0;0')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('255;255;255')).to.eql([255, 255, 255]); + expect(gdjs.rgbOrHexToRGBColor('120;12;6')).to.eql([120, 12, 6]); + }); + it('should max rgb values', function () { + expect(gdjs.rgbOrHexToRGBColor('255;255;300')).to.eql([255, 255, 255]); + expect(gdjs.rgbOrHexToRGBColor('999;12;6')).to.eql([255, 12, 6]); + }); + it('should cut rgb values if string too long', function () { + expect(gdjs.rgbOrHexToRGBColor('255;255;200456')).to.eql([ + 255, + 255, + 200, + ]); + }); + it('should return components for black if unrecognized input', function () { + expect(gdjs.rgbOrHexToRGBColor('NaN')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('19819830803')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('Infinity')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('-4564')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('9999;12;6')).to.eql([0, 0, 0]); + }); + }); + }); +}); diff --git a/newIDE/app/src/EffectsList/index.js b/newIDE/app/src/EffectsList/index.js index ff2c308f0da0..8fa34fbfa479 100644 --- a/newIDE/app/src/EffectsList/index.js +++ b/newIDE/app/src/EffectsList/index.js @@ -296,6 +296,7 @@ const Effect = React.forwardRef( Date: Mon, 23 Sep 2024 15:07:31 +0200 Subject: [PATCH 2/3] Improve Quick Customization flow (#6978) * Simplify the number of objects suggested for replacing * Simplify the number of behavior properties suggested for tweaking * New section to update the in-game title * Simplify publication at the end of the flow --- .vscode/settings.json | 3 +- .../Project/BehaviorConfigurationContainer.h | 59 +++- Core/GDCore/Project/Layout.cpp | 271 +++++++++++------- Core/GDCore/Project/Object.cpp | 76 +++-- Core/GDCore/Project/QuickCustomization.h | 23 +- ...uickCustomizationVisibilitiesContainer.cpp | 58 ++++ .../QuickCustomizationVisibilitiesContainer.h | 29 ++ Extensions/TextObject/TextObject.cpp | 39 ++- GDevelop.js/Bindings/Bindings.idl | 9 + GDevelop.js/types.d.ts | 7 + GDevelop.js/types/gdbehavior.js | 1 + GDevelop.js/types/gdbehaviorsshareddata.js | 1 + ...quickcustomizationvisibilitiescontainer.js | 7 + GDevelop.js/types/libgdevelop.js | 1 + .../quick_customization/replace_objects.svg | 148 ---------- .../quick_customization/tweak_gameplay.svg | 78 ----- .../app/src/AssetStore/AssetStoreContext.js | 1 - .../app/src/AssetStore/AssetStoreNavigator.js | 2 - .../app/src/AssetStore/AssetSwappingDialog.js | 128 +++++---- newIDE/app/src/AssetStore/AssetsList.js | 4 +- newIDE/app/src/AssetStore/ShopTiles.js | 17 +- newIDE/app/src/AssetStore/index.js | 253 ++++++++-------- newIDE/app/src/BehaviorsEditor/index.js | 46 +++ .../PropertiesMapToCompactSchema.js | 72 +++-- .../app/src/CompactPropertiesEditor/index.js | 71 ++--- .../ExtensionOptionsEditor.js | 2 +- .../OnlineWebExport/OnlineGameLink.js | 8 +- .../OnlineWebExport/OnlineWebExportFlow.js | 1 + .../app/src/GameDashboard/ShareGameDialog.js | 4 +- .../CompactInstancePropertiesEditor/index.js | 10 +- .../EditorContainers/HomePage/index.js | 10 + newIDE/app/src/MainFrame/RouterContext.js | 3 +- newIDE/app/src/MainFrame/index.js | 90 ++++-- .../src/Profile/AuthenticatedUserProvider.js | 22 +- .../app/src/QuickCustomization/GameImage.js | 46 +++ .../app/src/QuickCustomization/PreviewLine.js | 60 ++++ .../QuickCustomization/PreviewLine.module.css | 5 + .../QuickBehaviorsTweaker.js | 85 +++--- .../QuickCustomizationDialog.js | 96 +++---- .../QuickCustomizationGameTiles.js | 1 + ...CustomizationPropertiesVisibilityDialog.js | 114 ++++++++ .../QuickCustomization/QuickObjectReplacer.js | 45 +-- .../src/QuickCustomization/QuickPublish.js | 264 +++++++++-------- .../QuickPublish.module.css | 23 +- .../QuickCustomization/QuickTitleTweaker.js | 188 ++++++++++++ newIDE/app/src/QuickCustomization/TipCard.js | 39 +++ newIDE/app/src/QuickCustomization/index.js | 87 ++---- .../index.js | 6 - newIDE/app/src/UI/Accordion.js | 7 +- newIDE/app/src/UI/CompactToggleField/index.js | 57 +--- .../app/src/UI/CustomSvgIcons/PlaySquared.js | 26 ++ newIDE/app/src/UI/Dialog.js | 16 +- .../app/src/Utils/GDevelopServices/Example.js | 1 + .../QuickPublish.stories.js | 30 +- ...ctResourceSelectorWithThumbnail.stories.js | 19 -- .../Share/OnlineGameLink.stories.js | 9 + .../UI/CompactToggleField.stories.js | 24 +- 57 files changed, 1686 insertions(+), 1116 deletions(-) create mode 100644 Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp create mode 100644 Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h create mode 100644 GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js delete mode 100644 newIDE/app/public/res/quick_customization/replace_objects.svg delete mode 100644 newIDE/app/public/res/quick_customization/tweak_gameplay.svg create mode 100644 newIDE/app/src/QuickCustomization/GameImage.js create mode 100644 newIDE/app/src/QuickCustomization/PreviewLine.js create mode 100644 newIDE/app/src/QuickCustomization/PreviewLine.module.css create mode 100644 newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js create mode 100644 newIDE/app/src/QuickCustomization/QuickTitleTweaker.js create mode 100644 newIDE/app/src/QuickCustomization/TipCard.js create mode 100644 newIDE/app/src/UI/CustomSvgIcons/PlaySquared.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d938e0f89b6..9785cecd70dd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -114,7 +114,8 @@ "__bits": "cpp", "__verbose_abort": "cpp", "variant": "cpp", - "charconv": "cpp" + "charconv": "cpp", + "execution": "cpp" }, "files.exclude": { "Binaries/*build*": true, diff --git a/Core/GDCore/Project/BehaviorConfigurationContainer.h b/Core/GDCore/Project/BehaviorConfigurationContainer.h index 87e2fae1514e..9bdf4311bfa2 100644 --- a/Core/GDCore/Project/BehaviorConfigurationContainer.h +++ b/Core/GDCore/Project/BehaviorConfigurationContainer.h @@ -8,8 +8,10 @@ #include #include -#include "GDCore/Serialization/Serializer.h" + #include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Project/QuickCustomizationVisibilitiesContainer.h" +#include "GDCore/Serialization/Serializer.h" #include "GDCore/String.h" namespace gd { @@ -32,12 +34,21 @@ namespace gd { */ class GD_CORE_API BehaviorConfigurationContainer { public: - BehaviorConfigurationContainer() : folded(false), quickCustomizationVisibility(QuickCustomization::Visibility::Default){}; + BehaviorConfigurationContainer() + : folded(false), + quickCustomizationVisibility(QuickCustomization::Visibility::Default), + propertiesQuickCustomizationVisibilities() {}; BehaviorConfigurationContainer(const gd::String& name_, const gd::String& type_) - : name(name_), type(type_), folded(false), quickCustomizationVisibility(QuickCustomization::Visibility::Default){}; + : name(name_), + type(type_), + folded(false), + quickCustomizationVisibility(QuickCustomization::Visibility::Default), + propertiesQuickCustomizationVisibilities() {}; virtual ~BehaviorConfigurationContainer(); - virtual BehaviorConfigurationContainer* Clone() const { return new BehaviorConfigurationContainer(*this); } + virtual BehaviorConfigurationContainer* Clone() const { + return new BehaviorConfigurationContainer(*this); + } /** * \brief Return the name identifying the behavior @@ -68,7 +79,6 @@ class GD_CORE_API BehaviorConfigurationContainer { */ std::map GetProperties() const; - /** * \brief Called when the IDE wants to update a custom property of the * behavior @@ -84,9 +94,7 @@ class GD_CORE_API BehaviorConfigurationContainer { * \brief Called to initialize the content with the default properties * for the behavior. */ - virtual void InitializeContent() { - InitializeContent(content); - }; + virtual void InitializeContent() { InitializeContent(content); }; /** * \brief Serialize the behavior content. @@ -115,15 +123,42 @@ class GD_CORE_API BehaviorConfigurationContainer { */ bool IsFolded() const { return folded; } - void SetQuickCustomizationVisibility(QuickCustomization::Visibility visibility) { + /** + * @brief Set if the whole behavior should be visible or not in the Quick + * Customization. + */ + void SetQuickCustomizationVisibility( + QuickCustomization::Visibility visibility) { quickCustomizationVisibility = visibility; } + /** + * @brief Get if the whole behavior should be visible or not in the Quick + * Customization. + */ QuickCustomization::Visibility GetQuickCustomizationVisibility() const { return quickCustomizationVisibility; } -protected: + /** + * @brief Get the map of properties and their visibility in the Quick + * Customization. + */ + QuickCustomizationVisibilitiesContainer& + GetPropertiesQuickCustomizationVisibilities() { + return propertiesQuickCustomizationVisibilities; + } + + /** + * @brief Get the map of properties and their visibility in the Quick + * Customization. + */ + const QuickCustomizationVisibilitiesContainer& + GetPropertiesQuickCustomizationVisibilities() const { + return propertiesQuickCustomizationVisibilities; + } + + protected: /** * \brief Called when the IDE wants to know about the custom properties of the * behavior. @@ -159,7 +194,7 @@ class GD_CORE_API BehaviorConfigurationContainer { * \brief Called to initialize the content with the default properties * for the behavior. */ - virtual void InitializeContent(gd::SerializerElement& behaviorContent){}; + virtual void InitializeContent(gd::SerializerElement& behaviorContent) {}; private: gd::String name; ///< Name of the behavior @@ -169,6 +204,8 @@ class GD_CORE_API BehaviorConfigurationContainer { gd::SerializerElement content; // Storage for the behavior properties bool folded; QuickCustomization::Visibility quickCustomizationVisibility; + QuickCustomizationVisibilitiesContainer + propertiesQuickCustomizationVisibilities; }; } // namespace gd diff --git a/Core/GDCore/Project/Layout.cpp b/Core/GDCore/Project/Layout.cpp index 9c9ea5bcdcdd..3e1eb344c8d1 100644 --- a/Core/GDCore/Project/Layout.cpp +++ b/Core/GDCore/Project/Layout.cpp @@ -24,10 +24,11 @@ #include "GDCore/Project/ObjectGroup.h" #include "GDCore/Project/ObjectGroupsContainer.h" #include "GDCore/Project/Project.h" +#include "GDCore/Project/QuickCustomization.h" #include "GDCore/Serialization/SerializerElement.h" #include "GDCore/String.h" -#include "GDCore/Tools/PolymorphicClone.h" #include "GDCore/Tools/Log.h" +#include "GDCore/Tools/PolymorphicClone.h" using namespace std; @@ -43,7 +44,7 @@ Layout& Layout::operator=(const Layout& other) { return *this; } -Layout::~Layout(){}; +Layout::~Layout() {}; Layout::Layout() : backgroundColorR(209), @@ -52,9 +53,7 @@ Layout::Layout() stopSoundsOnStartup(true), standardSortMethod(true), disableInputWhenNotFocused(true), - variables(gd::VariablesContainer::SourceType::Scene) -{ -} + variables(gd::VariablesContainer::SourceType::Scene) {} void Layout::SetName(const gd::String& name_) { name = name_; @@ -102,7 +101,9 @@ const gd::Layer& Layout::GetLayer(const gd::String& name) const { return layers.GetLayer(name); } -gd::Layer& Layout::GetLayer(std::size_t index) { return layers.GetLayer(index); } +gd::Layer& Layout::GetLayer(std::size_t index) { + return layers.GetLayer(index); +} const gd::Layer& Layout::GetLayer(std::size_t index) const { return layers.GetLayer(index); @@ -125,9 +126,7 @@ void Layout::InsertLayer(const gd::Layer& layer, std::size_t position) { layers.InsertLayer(layer, position); } -void Layout::RemoveLayer(const gd::String& name) { - layers.RemoveLayer(name); -} +void Layout::RemoveLayer(const gd::String& name) { layers.RemoveLayer(name); } void Layout::SwapLayers(std::size_t firstLayerIndex, std::size_t secondLayerIndex) { @@ -153,7 +152,7 @@ void Layout::UpdateBehaviorsSharedData(gd::Project& project) { allBehaviorsNames.push_back(behavior.GetName()); } } - auto &globalObjects = project.GetObjects(); + auto& globalObjects = project.GetObjects(); for (std::size_t i = 0; i < globalObjects.GetObjectsCount(); ++i) { std::vector objectBehaviors = globalObjects.GetObject(i).GetAllBehaviorNames(); @@ -173,7 +172,8 @@ void Layout::UpdateBehaviorsSharedData(gd::Project& project) { if (behaviorsSharedData.find(name) != behaviorsSharedData.end()) continue; - auto sharedData = CreateBehaviorsSharedData(project, name, allBehaviorsTypes[i]); + auto sharedData = + CreateBehaviorsSharedData(project, name, allBehaviorsTypes[i]); if (sharedData) { behaviorsSharedData[name] = std::move(sharedData); } @@ -196,37 +196,39 @@ void Layout::UpdateBehaviorsSharedData(gd::Project& project) { } std::unique_ptr Layout::CreateBehaviorsSharedData( - gd::Project& project, const gd::String& name, const gd::String& behaviorsType) { - if (project.HasEventsBasedBehavior(behaviorsType)) { - auto sharedData = - gd::make_unique(name, project, behaviorsType); - sharedData->InitializeContent(); - return std::move(sharedData); - } - const gd::BehaviorMetadata& behaviorMetadata = - gd::MetadataProvider::GetBehaviorMetadata( - project.GetCurrentPlatform(), - behaviorsType); - if (gd::MetadataProvider::IsBadBehaviorMetadata(behaviorMetadata)) { - gd::LogWarning("Tried to create a behavior shared data with an unknown type: " + - behaviorsType + " on object " + GetName() + "!"); + gd::Project& project, + const gd::String& name, + const gd::String& behaviorsType) { + if (project.HasEventsBasedBehavior(behaviorsType)) { + auto sharedData = gd::make_unique( + name, project, behaviorsType); + sharedData->InitializeContent(); + return std::move(sharedData); + } + const gd::BehaviorMetadata& behaviorMetadata = + gd::MetadataProvider::GetBehaviorMetadata(project.GetCurrentPlatform(), + behaviorsType); + if (gd::MetadataProvider::IsBadBehaviorMetadata(behaviorMetadata)) { + gd::LogWarning( + "Tried to create a behavior shared data with an unknown type: " + + behaviorsType + " on object " + GetName() + "!"); // It's probably an events-based behavior that was removed. // Create a custom behavior shared data to preserve the properties values. - auto sharedData = - gd::make_unique(name, project, behaviorsType); - sharedData->InitializeContent(); - return std::move(sharedData); - } + auto sharedData = gd::make_unique( + name, project, behaviorsType); + sharedData->InitializeContent(); + return std::move(sharedData); + } - gd::BehaviorsSharedData* behaviorsSharedDataBluePrint = - behaviorMetadata.GetSharedDataInstance(); - if (!behaviorsSharedDataBluePrint) return nullptr; + gd::BehaviorsSharedData* behaviorsSharedDataBluePrint = + behaviorMetadata.GetSharedDataInstance(); + if (!behaviorsSharedDataBluePrint) return nullptr; - auto sharedData = behaviorsSharedDataBluePrint->Clone(); - sharedData->SetName(name); - sharedData->SetTypeName(behaviorsType); - sharedData->InitializeContent(); - return std::unique_ptr(sharedData); + auto sharedData = behaviorsSharedDataBluePrint->Clone(); + sharedData->SetName(name); + sharedData->SetTypeName(behaviorsType); + sharedData->InitializeContent(); + return std::unique_ptr(sharedData); } void Layout::SerializeTo(SerializerElement& element) const { @@ -243,11 +245,13 @@ void Layout::SerializeTo(SerializerElement& element) const { editorSettings.SerializeTo(element.AddChild("uiSettings")); - objectsContainer.GetObjectGroups().SerializeTo(element.AddChild("objectsGroups")); + objectsContainer.GetObjectGroups().SerializeTo( + element.AddChild("objectsGroups")); GetVariables().SerializeTo(element.AddChild("variables")); GetInitialInstances().SerializeTo(element.AddChild("instances")); objectsContainer.SerializeObjectsTo(element.AddChild("objects")); - objectsContainer.SerializeFoldersTo(element.AddChild("objectsFolderStructure")); + objectsContainer.SerializeFoldersTo( + element.AddChild("objectsFolderStructure")); gd::EventsListSerialization::SerializeEventsTo(events, element.AddChild("events")); @@ -257,15 +261,33 @@ void Layout::SerializeTo(SerializerElement& element) const { element.AddChild("behaviorsSharedData"); behaviorDatasElement.ConsiderAsArrayOf("behaviorSharedData"); for (const auto& it : behaviorsSharedData) { + const gd::BehaviorsSharedData& sharedData = *it.second; SerializerElement& dataElement = behaviorDatasElement.AddChild("behaviorSharedData"); - it.second->SerializeTo(dataElement); + sharedData.SerializeTo(dataElement); dataElement.RemoveChild("type"); // The content can contain type or name // properties, remove them. dataElement.RemoveChild("name"); - dataElement.SetAttribute("type", it.second->GetTypeName()); - dataElement.SetAttribute("name", it.second->GetName()); + dataElement.SetAttribute("type", sharedData.GetTypeName()); + dataElement.SetAttribute("name", sharedData.GetName()); + + // Handle Quick Customization info. + dataElement.RemoveChild("propertiesQuickCustomizationVisibilities"); + const QuickCustomizationVisibilitiesContainer& + propertiesQuickCustomizationVisibilities = + sharedData.GetPropertiesQuickCustomizationVisibilities(); + if (!propertiesQuickCustomizationVisibilities.IsEmpty()) { + propertiesQuickCustomizationVisibilities.SerializeTo( + dataElement.AddChild("propertiesQuickCustomizationVisibilities")); + } + const QuickCustomization::Visibility visibility = + sharedData.GetQuickCustomizationVisibility(); + if (visibility != QuickCustomization::Visibility::Default) { + dataElement.SetAttribute( + "quickCustomizationVisibility", + QuickCustomization::VisibilityAsString(visibility)); + } } } @@ -289,9 +311,11 @@ void Layout::UnserializeFrom(gd::Project& project, gd::EventsListSerialization::UnserializeEventsFrom( project, GetEvents(), element.GetChild("events", 0, "Events")); - objectsContainer.UnserializeObjectsFrom(project, element.GetChild("objects", 0, "Objets")); + objectsContainer.UnserializeObjectsFrom( + project, element.GetChild("objects", 0, "Objets")); if (element.HasChild("objectsFolderStructure")) { - objectsContainer.UnserializeFoldersFrom(project, element.GetChild("objectsFolderStructure", 0)); + objectsContainer.UnserializeFoldersFrom( + project, element.GetChild("objectsFolderStructure", 0)); } objectsContainer.AddMissingObjectsInRootFolder(); @@ -321,7 +345,6 @@ void Layout::UnserializeFrom(gd::Project& project, "Behavior"); // Compatibility with GD <= 4 gd::String name = sharedDataElement.GetStringAttribute("name", "", "Name"); - auto sharedData = CreateBehaviorsSharedData(project, name, type); if (sharedData) { // Compatibility with GD <= 4.0.98 @@ -336,6 +359,21 @@ void Layout::UnserializeFrom(gd::Project& project, else { sharedData->UnserializeFrom(sharedDataElement); } + + // Handle Quick Customization info. + if (sharedDataElement.HasChild( + "propertiesQuickCustomizationVisibilities")) { + sharedData->GetPropertiesQuickCustomizationVisibilities() + .UnserializeFrom(sharedDataElement.GetChild( + "propertiesQuickCustomizationVisibilities")); + } + if (sharedDataElement.HasChild("quickCustomizationVisibility")) { + sharedData->SetQuickCustomizationVisibility( + QuickCustomization::StringAsVisibility( + sharedDataElement.GetStringAttribute( + "quickCustomizationVisibility"))); + } + behaviorsSharedData[name] = std::move(sharedData); } } @@ -390,8 +428,9 @@ gd::String GD_CORE_API GetTypeOfObject(const gd::ObjectsContainer& project, type = project.GetObject(name).GetType(); // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. else if (searchInGroups) { for (std::size_t i = 0; i < layout.GetObjectGroups().size(); ++i) { if (layout.GetObjectGroups()[i].GetName() == name) { @@ -448,11 +487,12 @@ gd::String GD_CORE_API GetTypeOfObject(const gd::ObjectsContainer& project, return type; } -void GD_CORE_API FilterBehaviorNamesFromObject( - const gd::Object &object, const gd::String &behaviorType, - std::vector &behaviorNames) { +void GD_CORE_API +FilterBehaviorNamesFromObject(const gd::Object& object, + const gd::String& behaviorType, + std::vector& behaviorNames) { for (size_t i = 0; i < behaviorNames.size();) { - auto &behaviorName = behaviorNames[i]; + auto& behaviorName = behaviorNames[i]; if (!object.HasBehaviorNamed(behaviorName) || object.GetBehavior(behaviorName).GetTypeName() != behaviorType) { behaviorNames.erase(behaviorNames.begin() + i); @@ -462,19 +502,21 @@ void GD_CORE_API FilterBehaviorNamesFromObject( } } -std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( - const gd::ObjectsContainer &project, const gd::ObjectsContainer &layout, - const gd::String &objectOrGroupName, const gd::String &behaviorType, - bool searchInGroups) { +std::vector GD_CORE_API +GetBehaviorNamesInObjectOrGroup(const gd::ObjectsContainer& project, + const gd::ObjectsContainer& layout, + const gd::String& objectOrGroupName, + const gd::String& behaviorType, + bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { - auto &object = layout.GetObject(objectOrGroupName); + auto& object = layout.GetObject(objectOrGroupName); auto behaviorNames = object.GetAllBehaviorNames(); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; } if (project.HasObjectNamed(objectOrGroupName)) { - auto &object = project.GetObject(objectOrGroupName); + auto& object = project.GetObject(objectOrGroupName); auto behaviorNames = object.GetAllBehaviorNames(); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; @@ -486,9 +528,10 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -497,7 +540,7 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( std::vector behaviorNames; return behaviorNames; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -510,15 +553,15 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( auto behaviorNames = GetBehaviorNamesInObjectOrGroup( project, layout, groupsObjects[0], behaviorType, false); for (size_t i = 1; i < groupsObjects.size(); i++) { - auto &objectName = groupsObjects[i]; + auto& objectName = groupsObjects[i]; if (layout.HasObjectNamed(objectName)) { - auto &object = layout.GetObject(objectName); + auto& object = layout.GetObject(objectName); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; } if (project.HasObjectNamed(objectName)) { - auto &object = project.GetObject(objectName); + auto& object = project.GetObject(objectName); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; } @@ -529,10 +572,10 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( return behaviorNames; } -bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, - const gd::ObjectsContainer &layout, - const gd::String &objectOrGroupName, - const gd::String &behaviorName, +bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer& project, + const gd::ObjectsContainer& layout, + const gd::String& objectOrGroupName, + const gd::String& behaviorName, bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { @@ -547,9 +590,10 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -557,7 +601,7 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } else { return false; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -566,9 +610,9 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } // Check that all objects have the behavior. - for (auto &&object : groupsObjects) { - if (!HasBehaviorInObjectOrGroup(project, layout, object, behaviorName, - false)) { + for (auto&& object : groupsObjects) { + if (!HasBehaviorInObjectOrGroup( + project, layout, object, behaviorName, false)) { return false; } } @@ -576,18 +620,18 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, - const gd::ObjectsContainer& layout, - gd::String objectOrGroupName, - gd::String behaviorName, - bool searchInGroups) { + const gd::ObjectsContainer& layout, + gd::String objectOrGroupName, + gd::String behaviorName, + bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { - auto &object = layout.GetObject(objectOrGroupName); + auto& object = layout.GetObject(objectOrGroupName); return object.HasBehaviorNamed(behaviorName) && object.GetBehavior(behaviorName).IsDefaultBehavior(); } if (project.HasObjectNamed(objectOrGroupName)) { - auto &object = project.GetObject(objectOrGroupName); + auto& object = project.GetObject(objectOrGroupName); return object.HasBehaviorNamed(behaviorName) && object.GetBehavior(behaviorName).IsDefaultBehavior(); } @@ -597,9 +641,10 @@ bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -607,7 +652,7 @@ bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, } else { return false; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -616,30 +661,32 @@ bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, } // Check that all objects have the same type. - for (auto &&object : groupsObjects) { - if (!IsDefaultBehavior(project, layout, object, behaviorName, - false)) { + for (auto&& object : groupsObjects) { + if (!IsDefaultBehavior(project, layout, object, behaviorName, false)) { return false; } } return true; } -gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContainer& project, - const gd::ObjectsContainer& layout, - const gd::String& objectOrGroupName, - const gd::String& behaviorName, - bool searchInGroups) { +gd::String GD_CORE_API +GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContainer& project, + const gd::ObjectsContainer& layout, + const gd::String& objectOrGroupName, + const gd::String& behaviorName, + bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { - auto &object = layout.GetObject(objectOrGroupName); - return object.HasBehaviorNamed(behaviorName) ? - object.GetBehavior(behaviorName).GetTypeName() : ""; + auto& object = layout.GetObject(objectOrGroupName); + return object.HasBehaviorNamed(behaviorName) + ? object.GetBehavior(behaviorName).GetTypeName() + : ""; } if (project.HasObjectNamed(objectOrGroupName)) { - auto &object = project.GetObject(objectOrGroupName); - return object.HasBehaviorNamed(behaviorName) ? - object.GetBehavior(behaviorName).GetTypeName() : ""; + auto& object = project.GetObject(objectOrGroupName); + return object.HasBehaviorNamed(behaviorName) + ? object.GetBehavior(behaviorName).GetTypeName() + : ""; } if (!searchInGroups) { @@ -647,9 +694,10 @@ gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContain } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -657,7 +705,7 @@ gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContain } else { return ""; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -668,9 +716,9 @@ gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContain // Check that all objects have the behavior with the same type. auto behaviorType = GetTypeOfBehaviorInObjectOrGroup( project, layout, groupsObjects[0], behaviorName, false); - for (auto &&object : groupsObjects) { - if (GetTypeOfBehaviorInObjectOrGroup(project, layout, object, behaviorName, - false) != behaviorType) { + for (auto&& object : groupsObjects) { + if (GetTypeOfBehaviorInObjectOrGroup( + project, layout, object, behaviorName, false) != behaviorType) { return ""; } } @@ -682,14 +730,14 @@ gd::String GD_CORE_API GetTypeOfBehavior(const gd::ObjectsContainer& project, gd::String name, bool searchInGroups) { for (std::size_t i = 0; i < layout.GetObjectsCount(); ++i) { - const auto &object = layout.GetObject(i); + const auto& object = layout.GetObject(i); if (object.HasBehaviorNamed(name)) { return object.GetBehavior(name).GetTypeName(); } } for (std::size_t i = 0; i < project.GetObjectsCount(); ++i) { - const auto &object = project.GetObject(i); + const auto& object = project.GetObject(i); if (object.HasBehaviorNamed(name)) { return object.GetBehavior(name).GetTypeName(); } @@ -726,8 +774,9 @@ GetBehaviorsOfObject(const gd::ObjectsContainer& project, } // Search in groups - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. if (searchInGroups) { for (std::size_t i = 0; i < layout.GetObjectGroups().size(); ++i) { if (layout.GetObjectGroups()[i].GetName() == name) { diff --git a/Core/GDCore/Project/Object.cpp b/Core/GDCore/Project/Object.cpp index 0b11d52d71ce..02ac3bbeb489 100644 --- a/Core/GDCore/Project/Object.cpp +++ b/Core/GDCore/Project/Object.cpp @@ -12,8 +12,9 @@ #include "GDCore/Project/CustomBehavior.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" -#include "GDCore/Serialization/SerializerElement.h" #include "GDCore/Project/PropertyDescriptor.h" +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" #include "GDCore/Tools/Log.h" #include "GDCore/Tools/UUID/UUID.h" @@ -27,8 +28,8 @@ Object::Object(const gd::String& name_, : name(name_), configuration(std::move(configuration_)), objectVariables(gd::VariablesContainer::SourceType::Object) { - SetType(type_); - } + SetType(type_); +} Object::Object(const gd::String& name_, const gd::String& type_, @@ -36,8 +37,8 @@ Object::Object(const gd::String& name_, : name(name_), configuration(configuration_), objectVariables(gd::VariablesContainer::SourceType::Object) { - SetType(type_); - } + SetType(type_); +} void Object::Init(const gd::Object& object) { persistentUuid = object.persistentUuid; @@ -54,9 +55,7 @@ void Object::Init(const gd::Object& object) { configuration = object.configuration->Clone(); } -gd::ObjectConfiguration& Object::GetConfiguration() { - return *configuration; -} +gd::ObjectConfiguration& Object::GetConfiguration() { return *configuration; } const gd::ObjectConfiguration& Object::GetConfiguration() const { return *configuration; @@ -77,8 +76,7 @@ bool Object::RenameBehavior(const gd::String& name, const gd::String& newName) { behaviors.find(newName) != behaviors.end()) return false; - std::unique_ptr aut = - std::move(behaviors.find(name)->second); + std::unique_ptr aut = std::move(behaviors.find(name)->second); behaviors.erase(name); behaviors[newName] = std::move(aut); behaviors[newName]->SetName(newName); @@ -99,10 +97,10 @@ bool Object::HasBehaviorNamed(const gd::String& name) const { } gd::Behavior* Object::AddNewBehavior(const gd::Project& project, - const gd::String& type, - const gd::String& name) { - auto initializeAndAdd = - [this, &name](std::unique_ptr behavior) { + const gd::String& type, + const gd::String& name) { + auto initializeAndAdd = [this, + &name](std::unique_ptr behavior) { behavior->InitializeContent(); this->behaviors[name] = std::move(behavior); return this->behaviors[name].get(); @@ -111,18 +109,17 @@ gd::Behavior* Object::AddNewBehavior(const gd::Project& project, if (project.HasEventsBasedBehavior(type)) { return initializeAndAdd( gd::make_unique(name, project, type)); - } - else { + } else { const gd::BehaviorMetadata& behaviorMetadata = gd::MetadataProvider::GetBehaviorMetadata(project.GetCurrentPlatform(), type); if (gd::MetadataProvider::IsBadBehaviorMetadata(behaviorMetadata)) { - gd::LogWarning("Tried to create a behavior with an unknown type: " + type - + " on object " + GetName() + "!"); - // It's probably an events-based behavior that was removed. - // Create a custom behavior to preserve the properties values. - return initializeAndAdd( - gd::make_unique(name, project, type)); + gd::LogWarning("Tried to create a behavior with an unknown type: " + + type + " on object " + GetName() + "!"); + // It's probably an events-based behavior that was removed. + // Create a custom behavior to preserve the properties values. + return initializeAndAdd( + gd::make_unique(name, project, type)); } std::unique_ptr behavior(behaviorMetadata.Get().Clone()); behavior->SetName(name); @@ -196,6 +193,20 @@ void Object::UnserializeFrom(gd::Project& project, else { behavior->UnserializeFrom(behaviorElement); } + + // Handle Quick Customization info. + if (behaviorElement.HasChild( + "propertiesQuickCustomizationVisibilities")) { + behavior->GetPropertiesQuickCustomizationVisibilities().UnserializeFrom( + behaviorElement.GetChild( + "propertiesQuickCustomizationVisibilities")); + } + if (behaviorElement.HasChild("quickCustomizationVisibility")) { + behavior->SetQuickCustomizationVisibility( + QuickCustomization::StringAsVisibility( + behaviorElement.GetStringAttribute( + "quickCustomizationVisibility"))); + } } } @@ -217,8 +228,8 @@ void Object::SerializeTo(SerializerElement& element) const { std::vector allBehaviors = GetAllBehaviorNames(); for (std::size_t i = 0; i < allBehaviors.size(); ++i) { const gd::Behavior& behavior = GetBehavior(allBehaviors[i]); - // Default behaviors are added at the object creation according to metadata. - // They don't need to be serialized. + // Default behaviors are added at the object creation according to + // metadata. They don't need to be serialized. if (behavior.IsDefaultBehavior()) { continue; } @@ -230,6 +241,23 @@ void Object::SerializeTo(SerializerElement& element) const { behaviorElement.RemoveChild("name"); behaviorElement.SetAttribute("type", behavior.GetTypeName()); behaviorElement.SetAttribute("name", behavior.GetName()); + + // Handle Quick Customization info. + behaviorElement.RemoveChild("propertiesQuickCustomizationVisibilities"); + const QuickCustomizationVisibilitiesContainer& + propertiesQuickCustomizationVisibilities = + behavior.GetPropertiesQuickCustomizationVisibilities(); + if (!propertiesQuickCustomizationVisibilities.IsEmpty()) { + propertiesQuickCustomizationVisibilities.SerializeTo( + behaviorElement.AddChild("propertiesQuickCustomizationVisibilities")); + } + const QuickCustomization::Visibility visibility = + behavior.GetQuickCustomizationVisibility(); + if (visibility != QuickCustomization::Visibility::Default) { + behaviorElement.SetAttribute( + "quickCustomizationVisibility", + QuickCustomization::VisibilityAsString(visibility)); + } } configuration->SerializeTo(element); diff --git a/Core/GDCore/Project/QuickCustomization.h b/Core/GDCore/Project/QuickCustomization.h index c09c102d6d30..4b162fb3d144 100644 --- a/Core/GDCore/Project/QuickCustomization.h +++ b/Core/GDCore/Project/QuickCustomization.h @@ -1,16 +1,37 @@ #pragma once +#include "GDCore/String.h" + namespace gd { class QuickCustomization { public: enum Visibility { - /** Visibility based on the parent or editor heuristics (probably visible). */ + /** Visibility based on the parent or editor heuristics (probably visible). + */ Default, /** Visible in the quick customization editor. */ Visible, /** Not visible in the quick customization editor. */ Hidden }; + + static Visibility StringAsVisibility(const gd::String& str) { + if (str == "visible") + return Visibility::Visible; + else if (str == "hidden") + return Visibility::Hidden; + + return Visibility::Default; + } + + static gd::String VisibilityAsString(Visibility visibility) { + if (visibility == Visibility::Visible) + return "visible"; + else if (visibility == Visibility::Hidden) + return "hidden"; + + return "default"; + } }; } // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp new file mode 100644 index 000000000000..aa6d702fbc09 --- /dev/null +++ b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp @@ -0,0 +1,58 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/Project/QuickCustomizationVisibilitiesContainer.h" + +#include +#include + +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/String.h" +#include "GDCore/Tools/UUID/UUID.h" + +using namespace std; + +namespace gd { + +QuickCustomizationVisibilitiesContainer:: + QuickCustomizationVisibilitiesContainer() {} + +bool QuickCustomizationVisibilitiesContainer::IsEmpty() const { + return visibilities.empty(); +} + +void QuickCustomizationVisibilitiesContainer::Set( + const gd::String& name, QuickCustomization::Visibility visibility) { + visibilities[name] = visibility; +} + +QuickCustomization::Visibility QuickCustomizationVisibilitiesContainer::Get( + const gd::String& name) const { + auto it = visibilities.find(name); + if (it != visibilities.end()) return it->second; + + return QuickCustomization::Visibility::Default; +} + +void QuickCustomizationVisibilitiesContainer::SerializeTo( + SerializerElement& element) const { + for (auto& visibility : visibilities) { + element.SetStringAttribute( + visibility.first, + QuickCustomization::VisibilityAsString(visibility.second)); + } +} + +void QuickCustomizationVisibilitiesContainer::UnserializeFrom( + const SerializerElement& element) { + visibilities.clear(); + for (auto& child : element.GetAllChildren()) { + visibilities[child.first] = + QuickCustomization::StringAsVisibility(child.second->GetStringValue()); + } +} + +} // namespace gd diff --git a/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h new file mode 100644 index 000000000000..56794d5ef224 --- /dev/null +++ b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/String.h" + +namespace gd { + +class QuickCustomizationVisibilitiesContainer { + public: + QuickCustomizationVisibilitiesContainer(); + + void Set(const gd::String& name, QuickCustomization::Visibility visibility); + + QuickCustomization::Visibility Get(const gd::String& name) const; + + bool IsEmpty() const; + + void SerializeTo(SerializerElement& element) const; + + void UnserializeFrom(const SerializerElement& element); + + private: + std::map visibilities; +}; + +} // namespace gd \ No newline at end of file diff --git a/Extensions/TextObject/TextObject.cpp b/Extensions/TextObject/TextObject.cpp index bead7d3c05cd..7806ff8c3e66 100644 --- a/Extensions/TextObject/TextObject.cpp +++ b/Extensions/TextObject/TextObject.cpp @@ -129,19 +129,22 @@ std::map TextObject::GetProperties() const { .SetType("resource") .AddExtraInfo("font") .SetLabel(_("Font")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["bold"] .SetValue(bold ? "true" : "false") .SetType("boolean") .SetLabel(_("Bold")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["italic"] .SetValue(italic ? "true" : "false") .SetType("boolean") .SetLabel(_("Italic")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["color"] .SetValue(color) @@ -156,66 +159,76 @@ std::map TextObject::GetProperties() const { .AddExtraInfo("center") .AddExtraInfo("right") .SetLabel(_("Alignment, when multiple lines are displayed")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["isOutlineEnabled"] .SetValue(isOutlineEnabled ? "true" : "false") .SetType("boolean") .SetLabel(_("Outline")) - .SetGroup(_("Outline")); + .SetGroup(_("Outline")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["outlineColor"] .SetValue(outlineColor) .SetType("color") .SetLabel(_("Outline color")) - .SetGroup(_("Outline")); + .SetGroup(_("Outline")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["outlineThickness"] .SetValue(gd::String::From(outlineThickness)) .SetType("number") .SetLabel(_("Outline thickness")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) - .SetGroup(_("Outline")); + .SetGroup(_("Outline")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["isShadowEnabled"] .SetValue(isShadowEnabled ? "true" : "false") .SetType("boolean") .SetLabel(_("Shadow")) - .SetGroup(_("Shadow")); + .SetGroup(_("Shadow")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowColor"] .SetValue(shadowColor) .SetType("color") .SetLabel(_("Shadow color")) - .SetGroup(_("Shadow")); + .SetGroup(_("Shadow")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowOpacity"] .SetValue(gd::String::From(shadowOpacity)) .SetType("number") .SetLabel(_("Shadow opacity")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) - .SetGroup(_("Shadow")); + .SetGroup(_("Shadow")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowAngle"] .SetValue(gd::String::From(shadowAngle)) .SetType("number") .SetLabel(_("Shadow angle")) .SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle()) - .SetGroup(_("Shadow")); + .SetGroup(_("Shadow")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowDistance"] .SetValue(gd::String::From(shadowDistance)) .SetType("number") .SetLabel(_("Shadow distance")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) - .SetGroup(_("Shadow")); + .SetGroup(_("Shadow")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowBlurRadius"] .SetValue(gd::String::From(shadowBlurRadius)) .SetType("number") .SetLabel(_("Shadow blur radius")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) - .SetGroup(_("Shadow")); + .SetGroup(_("Shadow")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); return objectProperties; } diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 5ac7554ba26f..3fd78b745593 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -750,6 +750,8 @@ interface Behavior { void SetFolded(boolean folded); boolean IsDefaultBehavior(); + + [Ref] QuickCustomizationVisibilitiesContainer GetPropertiesQuickCustomizationVisibilities(); }; [JSImplementation=Behavior] @@ -771,6 +773,8 @@ interface BehaviorsSharedData { [Value] MapStringPropertyDescriptor GetProperties(); boolean UpdateProperty([Const] DOMString name, [Const] DOMString value); void InitializeContent(); + + [Ref] QuickCustomizationVisibilitiesContainer GetPropertiesQuickCustomizationVisibilities(); }; [JSImplementation=BehaviorsSharedData] @@ -1915,6 +1919,11 @@ interface QuickCustomization { // Nothing, it's just a container for the visibility enum. }; +interface QuickCustomizationVisibilitiesContainer { + void Set([Const] DOMString name, QuickCustomization_Visibility visibility); + QuickCustomization_Visibility Get([Const] DOMString name); +}; + interface BehaviorMetadata { [Const, Ref] DOMString GetName(); [Const, Ref] DOMString GetFullName(); diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index b36d71d9fce4..febb9007e57f 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -649,6 +649,7 @@ export class Behavior extends EmscriptenObject { isFolded(): boolean; setFolded(folded: boolean): void; isDefaultBehavior(): boolean; + getPropertiesQuickCustomizationVisibilities(): QuickCustomizationVisibilitiesContainer; } export class BehaviorJsImplementation extends Behavior { @@ -666,6 +667,7 @@ export class BehaviorsSharedData extends EmscriptenObject { getProperties(): MapStringPropertyDescriptor; updateProperty(name: string, value: string): boolean; initializeContent(): void; + getPropertiesQuickCustomizationVisibilities(): QuickCustomizationVisibilitiesContainer; } export class BehaviorSharedDataJsImplementation extends BehaviorsSharedData { @@ -1530,6 +1532,11 @@ export class QuickCustomization extends EmscriptenObject {static Default = 0; static Hidden = 2; } +export class QuickCustomizationVisibilitiesContainer extends EmscriptenObject { + set(name: string, visibility: QuickCustomization_Visibility): void; + get(name: string): QuickCustomization_Visibility; +} + export class BehaviorMetadata extends EmscriptenObject { getName(): string; getFullName(): string; diff --git a/GDevelop.js/types/gdbehavior.js b/GDevelop.js/types/gdbehavior.js index 6392464c7f71..6be63e427cc4 100644 --- a/GDevelop.js/types/gdbehavior.js +++ b/GDevelop.js/types/gdbehavior.js @@ -13,6 +13,7 @@ declare class gdBehavior { isFolded(): boolean; setFolded(folded: boolean): void; isDefaultBehavior(): boolean; + getPropertiesQuickCustomizationVisibilities(): gdQuickCustomizationVisibilitiesContainer; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/gdbehaviorsshareddata.js b/GDevelop.js/types/gdbehaviorsshareddata.js index 28311eb336b2..1cdf78cfb382 100644 --- a/GDevelop.js/types/gdbehaviorsshareddata.js +++ b/GDevelop.js/types/gdbehaviorsshareddata.js @@ -7,6 +7,7 @@ declare class gdBehaviorsSharedData { getProperties(): gdMapStringPropertyDescriptor; updateProperty(name: string, value: string): boolean; initializeContent(): void; + getPropertiesQuickCustomizationVisibilities(): gdQuickCustomizationVisibilitiesContainer; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js b/GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js new file mode 100644 index 000000000000..26aa5580709d --- /dev/null +++ b/GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js @@ -0,0 +1,7 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdQuickCustomizationVisibilitiesContainer { + set(name: string, visibility: QuickCustomization_Visibility): void; + get(name: string): QuickCustomization_Visibility; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js index ae7f433578a0..a707d41a7e7a 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -151,6 +151,7 @@ declare class libGDevelop { ObjectMetadata: Class; QuickCustomization_Visibility: Class; QuickCustomization: Class; + QuickCustomizationVisibilitiesContainer: Class; BehaviorMetadata: Class; EffectMetadata: Class; EventMetadata: Class; diff --git a/newIDE/app/public/res/quick_customization/replace_objects.svg b/newIDE/app/public/res/quick_customization/replace_objects.svg deleted file mode 100644 index 5f68c8b21057..000000000000 --- a/newIDE/app/public/res/quick_customization/replace_objects.svg +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/newIDE/app/public/res/quick_customization/tweak_gameplay.svg b/newIDE/app/public/res/quick_customization/tweak_gameplay.svg deleted file mode 100644 index 01777a94378d..000000000000 --- a/newIDE/app/public/res/quick_customization/tweak_gameplay.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/newIDE/app/src/AssetStore/AssetStoreContext.js b/newIDE/app/src/AssetStore/AssetStoreContext.js index 31743c3a60ae..b70276616753 100644 --- a/newIDE/app/src/AssetStore/AssetStoreContext.js +++ b/newIDE/app/src/AssetStore/AssetStoreContext.js @@ -134,7 +134,6 @@ export const initialAssetStoreState: AssetStoreState = { shopNavigationState: { getCurrentPage: () => assetStoreHomePageState, isRootPage: true, - isAssetSwappingHistory: false, backToPreviousPage: () => assetStoreHomePageState, openHome: () => assetStoreHomePageState, openAssetSwapping: () => assetStoreHomePageState, diff --git a/newIDE/app/src/AssetStore/AssetStoreNavigator.js b/newIDE/app/src/AssetStore/AssetStoreNavigator.js index 3c72fa116dd6..4f97cfe68a21 100644 --- a/newIDE/app/src/AssetStore/AssetStoreNavigator.js +++ b/newIDE/app/src/AssetStore/AssetStoreNavigator.js @@ -28,7 +28,6 @@ export type AssetStorePageState = {| export type NavigationState = {| getCurrentPage: () => AssetStorePageState, isRootPage: boolean, - isAssetSwappingHistory: boolean, backToPreviousPage: () => AssetStorePageState, openHome: () => AssetStorePageState, openAssetSwapping: () => AssetStorePageState, @@ -122,7 +121,6 @@ export const useShopNavigation = (): NavigationState => { () => ({ getCurrentPage: () => previousPages[previousPages.length - 1], isRootPage: previousPages.length <= 1, - isAssetSwappingHistory: !isHomePage(previousPages[0]), backToPreviousPage: () => { if (previousPages.length > 1) { const newPreviousPages = previousPages.slice( diff --git a/newIDE/app/src/AssetStore/AssetSwappingDialog.js b/newIDE/app/src/AssetStore/AssetSwappingDialog.js index 6fa0b697a6c1..3e679b2108e3 100644 --- a/newIDE/app/src/AssetStore/AssetSwappingDialog.js +++ b/newIDE/app/src/AssetStore/AssetSwappingDialog.js @@ -1,21 +1,19 @@ // @flow -import { Trans } from '@lingui/macro'; +import { t, Trans } from '@lingui/macro'; import { I18n } from '@lingui/react'; import * as React from 'react'; import Dialog from '../UI/Dialog'; import FlatButton from '../UI/FlatButton'; import { AssetStore, type AssetStoreInterface } from '.'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; -import RaisedButton from '../UI/RaisedButton'; import { AssetStoreContext } from './AssetStoreContext'; -import Window from '../Utils/Window'; import ErrorBoundary from '../UI/ErrorBoundary'; import LoaderModal from '../UI/LoaderModal'; import { useInstallAsset } from './NewObjectDialog'; import { swapAsset } from './AssetSwapper'; import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader'; - -const isDev = Window.isDev(); +import useAlertDialog from '../UI/Alert/useAlertDialog'; +import RaisedButton from '../UI/RaisedButton'; type Props = {| project: gdProject, @@ -25,6 +23,8 @@ type Props = {| object: gdObject, resourceManagementProps: ResourceManagementProps, onClose: ({ swappingDone: boolean }) => void, + // Use minimal UI to hide filters & the details page (useful for Quick Customization) + minimalUI?: boolean, |}; function AssetSwappingDialog({ @@ -35,10 +35,9 @@ function AssetSwappingDialog({ object, resourceManagementProps, onClose, + minimalUI, }: Props) { - const { shopNavigationState, environment, setEnvironment } = React.useContext( - AssetStoreContext - ); + const { shopNavigationState } = React.useContext(AssetStoreContext); const { openedAssetShortHeader } = shopNavigationState.getCurrentPage(); const [ @@ -50,33 +49,44 @@ function AssetSwappingDialog({ objectsContainer, resourceManagementProps, }); + const { showAlert } = useAlertDialog(); - const onInstallOpenedAsset = React.useCallback( + const installOpenedAsset = React.useCallback( async (): Promise => { if (!openedAssetShortHeader) return; setIsAssetBeingInstalled(true); - const installAssetOutput = await installAsset(openedAssetShortHeader); - if (!installAssetOutput) { - setIsAssetBeingInstalled(false); - return; - } + try { + const installAssetOutput = await installAsset(openedAssetShortHeader); + if (!installAssetOutput) { + throw new Error('Failed to install asset'); + } - if (installAssetOutput.createdObjects.length > 0) { - swapAsset( - project, - PixiResourcesLoader, - object, - installAssetOutput.createdObjects[0], - openedAssetShortHeader - ); - } - for (const createdObject of installAssetOutput.createdObjects) { - objectsContainer.removeObject(createdObject.getName()); - } + if (installAssetOutput.createdObjects.length > 0) { + swapAsset( + project, + PixiResourcesLoader, + object, + installAssetOutput.createdObjects[0], + openedAssetShortHeader + ); + } + for (const createdObject of installAssetOutput.createdObjects) { + objectsContainer.removeObject(createdObject.getName()); + } - setIsAssetBeingInstalled(false); - onClose({ swappingDone: true }); + onClose({ swappingDone: true }); + } catch (err) { + showAlert({ + title: t`Could not swap asset`, + message: t`Something went wrong while swapping the asset. Please try again.`, + }); + console.error('Error while installing asset:', err); + } finally { + // Always go back to the previous page so the asset is unselected. + shopNavigationState.backToPreviousPage(); + setIsAssetBeingInstalled(false); + } }, [ installAsset, @@ -85,35 +95,36 @@ function AssetSwappingDialog({ objectsContainer, openedAssetShortHeader, onClose, + shopNavigationState, + showAlert, ] ); - const mainAction = openedAssetShortHeader ? ( - Adding... : Swap - } - onClick={onInstallOpenedAsset} - disabled={isAssetBeingInstalled} - id="swap-asset-button" - /> - ) : isDev ? ( - Show live assets - ) : ( - Show staging assets - ) + const mainAction = + openedAssetShortHeader && !minimalUI ? ( + Adding... : Swap + } + onClick={installOpenedAsset} + disabled={isAssetBeingInstalled} + id="swap-asset-button" + /> + ) : null; + + // Try to install the asset as soon as selected, if in minimal UI mode. + React.useEffect( + () => { + if (openedAssetShortHeader && !isAssetBeingInstalled && minimalUI) { + installOpenedAsset(); } - onClick={() => { - setEnvironment(environment === 'staging' ? 'live' : 'staging'); - }} - /> - ) : null; + }, + // Only run when the asset is selected and not already being installed. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isAssetBeingInstalled, openedAssetShortHeader] + ); const assetStore = React.useRef(null); const handleClose = React.useCallback( @@ -130,27 +141,30 @@ function AssetSwappingDialog({ <> Swap {object.getName()} with another asset} - actions={[ + actions={[mainAction]} + secondaryActions={[ Back} - primary={false} onClick={handleClose} id="close-button" + fullWidth + primary />, - mainAction, ]} + onApply={minimalUI ? undefined : installOpenedAsset} onRequestClose={handleClose} - onApply={onInstallOpenedAsset} open flexBody fullHeight id="asset-swapping-dialog" + actionsFullWidthOnMobile > {isAssetBeingInstalled && } diff --git a/newIDE/app/src/AssetStore/AssetsList.js b/newIDE/app/src/AssetStore/AssetsList.js index 2e68d40175c4..667223554d0c 100644 --- a/newIDE/app/src/AssetStore/AssetsList.js +++ b/newIDE/app/src/AssetStore/AssetsList.js @@ -48,7 +48,6 @@ import Breadcrumbs from '../UI/Breadcrumbs'; import { getFolderTagsFromAssetShortHeaders } from './TagsHelper'; import { PrivateGameTemplateStoreContext } from './PrivateGameTemplates/PrivateGameTemplateStoreContext'; import { type AssetStorePageState } from './AssetStoreNavigator'; -import RaisedButton from '../UI/RaisedButton'; import FlatButton from '../UI/FlatButton'; import HelpIcon from '../UI/HelpIcon'; import { OwnedProductLicense } from './ProductLicense/ProductLicenseOptions'; @@ -202,9 +201,8 @@ const PageBreakNavigation = ({ }} disabled={pageBreakIndex <= 0} /> - Show next assets} onClick={() => { currentPage.pageBreakIndex = (currentPage.pageBreakIndex || 0) + 1; diff --git a/newIDE/app/src/AssetStore/ShopTiles.js b/newIDE/app/src/AssetStore/ShopTiles.js index eba9eff670c0..c7e4e373da1f 100644 --- a/newIDE/app/src/AssetStore/ShopTiles.js +++ b/newIDE/app/src/AssetStore/ShopTiles.js @@ -522,13 +522,24 @@ export const ExampleTile = ({ onSelect, style, customTitle, + useQuickCustomizationThumbnail, }: {| exampleShortHeader: ExampleShortHeader | null, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, customTitle?: string, + useQuickCustomizationThumbnail?: boolean, |}) => { + const thumbnailImgUrl = exampleShortHeader + ? useQuickCustomizationThumbnail + ? exampleShortHeader.quickCustomizationImageUrl + ? exampleShortHeader.quickCustomizationImageUrl + : exampleShortHeader.previewImageUrls + ? exampleShortHeader.previewImageUrls[0] + : '' + : '' + : ''; const classesForGridListItem = useStylesForGridListItem(); return ( ) : ( diff --git a/newIDE/app/src/AssetStore/index.js b/newIDE/app/src/AssetStore/index.js index dd408354ac9b..cacd0040a61c 100644 --- a/newIDE/app/src/AssetStore/index.js +++ b/newIDE/app/src/AssetStore/index.js @@ -65,6 +65,7 @@ type Props = {| ) => void, onOpenProfile?: () => void, assetSwappedObject?: ?gdObject, + minimalUI?: boolean, |}; export type AssetStoreInterface = {| @@ -104,6 +105,7 @@ export const AssetStore = React.forwardRef( onOpenPrivateGameTemplateListingData, onOpenProfile, assetSwappedObject, + minimalUI, }: Props, ref ) => { @@ -147,11 +149,6 @@ export const AssetStore = React.forwardRef( } } assetSwappedObjectPtr.current = assetSwappedObject.ptr; - } else if (shopNavigationState.isAssetSwappingHistory) { - shopNavigationState.openHome(); - assetFiltersState.setAssetSwappingFilter( - new AssetSwappingAssetStoreSearchFilter() - ); } }, [ @@ -600,129 +597,138 @@ export const AssetStore = React.forwardRef( return ( - - { - setSearchText(''); - const page = assetSwappedObject - ? shopNavigationState.openAssetSwapping() - : shopNavigationState.openHome(); - setScrollUpdateIsNeeded(page); - clearAllAssetStoreFilters(); - setIsFiltersPanelOpen(false); - }} - size="small" - > - - - - { - if (searchText === newValue) { - return; + <> + + {!(assetSwappedObject && minimalUI) && ( + { + setSearchText(''); + const page = assetSwappedObject + ? shopNavigationState.openAssetSwapping() + : shopNavigationState.openHome(); + setScrollUpdateIsNeeded(page); + clearAllAssetStoreFilters(); + setIsFiltersPanelOpen(false); + }} + size="small" + > + + + )} + + { + if (searchText === newValue) { + return; } - } else { - // A new search is being initiated: navigate to the search page, - // and clear the history as a new search was launched. - if (!!newValue) { - shopNavigationState.clearHistory(); + setSearchText(newValue); + if (isOnSearchResultPage) { + // An existing search is already being done: just move to the + // top search results. shopNavigationState.openSearchResultPage(); - openFiltersPanelIfAppropriate(); + const assetsListInterface = assetsList.current; + if (assetsListInterface) { + assetsListInterface.scrollToPosition(0); + assetsListInterface.setPageBreakIndex(0); + } + } else { + // A new search is being initiated: navigate to the search page, + // and clear the history as a new search was launched. + if (!!newValue) { + shopNavigationState.clearHistory(); + shopNavigationState.openSearchResultPage(); + openFiltersPanelIfAppropriate(); + } } - } - }} - onRequestSearch={() => {}} - ref={searchBar} - id="asset-store-search-bar" - /> - - setIsFiltersPanelOpen(!isFiltersPanelOpen)} - disabled={!canShowFiltersPanel} - selected={canShowFiltersPanel && isFiltersPanelOpen} - size="small" - > - - - - + }} + onRequestSearch={() => {}} + ref={searchBar} + id="asset-store-search-bar" + /> + + {!(assetSwappedObject && minimalUI) && ( + setIsFiltersPanelOpen(!isFiltersPanelOpen)} + disabled={!canShowFiltersPanel} + selected={canShowFiltersPanel && isFiltersPanelOpen} + size="small" + > + + + )} + + + - {(!isOnHomePage || !!openedShopCategory) && ( - <> - {shopNavigationState.isRootPage ? null : ( - - } - label={Back} - onClick={async () => { - const page = shopNavigationState.backToPreviousPage(); - const isUpdatingSearchtext = reApplySearchTextIfNeeded( - page - ); - if (isUpdatingSearchtext) { - // Updating the search is not instant, so we cannot apply the scroll position - // right away. We force a wait as there's no easy way to know when results are completely updated. - await delay(500); - setScrollUpdateIsNeeded(page); - applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again. - } else { - setScrollUpdateIsNeeded(page); - } - }} - /> - - )} - {(openedAssetPack || - openedPrivateAssetPackListingData || - filtersState.chosenCategory) && ( - <> - {!openedAssetPack && !openedPrivateAssetPackListingData && ( - // Only show the category name if we're not on an asset pack page. - - - {filtersState.chosenCategory - ? capitalize(filtersState.chosenCategory.node.name) - : ''} - - - )} - - {openedAssetPack && - openedAssetPack.content && - doesAssetPackContainAudio(openedAssetPack) && - !isAssetPackAudioOnly(openedAssetPack) ? ( - - ) : null} + {(!isOnHomePage || !!openedShopCategory) && + !(assetSwappedObject && minimalUI) && ( + <> + {shopNavigationState.isRootPage ? null : ( + + } + label={Back} + onClick={async () => { + const page = shopNavigationState.backToPreviousPage(); + const isUpdatingSearchtext = reApplySearchTextIfNeeded( + page + ); + if (isUpdatingSearchtext) { + // Updating the search is not instant, so we cannot apply the scroll position + // right away. We force a wait as there's no easy way to know when results are completely updated. + await delay(500); + setScrollUpdateIsNeeded(page); + applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again. + } else { + setScrollUpdateIsNeeded(page); + } + }} + /> - - )} - - )} + )} + {(openedAssetPack || + openedPrivateAssetPackListingData || + filtersState.chosenCategory) && ( + <> + {!openedAssetPack && !openedPrivateAssetPackListingData && ( + // Only show the category name if we're not on an asset pack page. + + + {filtersState.chosenCategory + ? capitalize( + filtersState.chosenCategory.node.name + ) + : ''} + + + )} + + {openedAssetPack && + openedAssetPack.content && + doesAssetPackContainAudio(openedAssetPack) && + !isAssetPackAudioOnly(openedAssetPack) ? ( + + ) : null} + + + )} + + )} ( onGoBackToFolderIndex={goBackToFolderIndex} currentPage={shopNavigationState.getCurrentPage()} hideGameTemplates={hideGameTemplates} - hideDetails={!!assetSwappedObject} + hideDetails={!!assetSwappedObject && !!minimalUI} /> - ) : openedAssetShortHeader ? ( + ) : // Do not show the asset details if we're swapping an asset. + openedAssetShortHeader && !(assetSwappedObject && minimalUI) ? ( Promise, openExtension: (behaviorType: string) => void, + openBehaviorPropertiesQuickCustomizationDialog: ( + behaviorName: string + ) => void, |}; const BehaviorConfigurationEditor = React.forwardRef< @@ -95,6 +99,7 @@ const BehaviorConfigurationEditor = React.forwardRef< canPasteBehaviors, pasteBehaviors, openExtension, + openBehaviorPropertiesQuickCustomizationDialog, }, ref ) => { @@ -209,6 +214,18 @@ const BehaviorConfigurationEditor = React.forwardRef< }, ] : []), + ...(!Window.isDev() + ? [] + : [ + { type: 'separator' }, + { + label: i18n._(t`Quick Customization settings`), + click: () => + openBehaviorPropertiesQuickCustomizationDialog( + behaviorName + ), + }, + ]), ]} />, ]} @@ -310,6 +327,10 @@ const BehaviorsEditor = (props: Props) => { const [newBehaviorDialogOpen, setNewBehaviorDialogOpen] = React.useState( false ); + const [ + selectedQuickCustomizationPropertiesBehavior, + setSelectedQuickCustomizationPropertiesBehavior, + ] = React.useState(null); const showBehaviorOverridingConfirmation = useBehaviorOverridingAlertDialog(); @@ -582,6 +603,16 @@ const BehaviorsEditor = (props: Props) => { [openBehaviorEvents, project] ); + const openBehaviorPropertiesQuickCustomizationDialog = React.useCallback( + (behaviorName: string) => { + if (!object.hasBehaviorNamed(behaviorName)) return; + const behavior = object.getBehavior(behaviorName); + + setSelectedQuickCustomizationPropertiesBehavior(behavior); + }, + [object] + ); + const isClipboardContainingBehaviors = Clipboard.has( BEHAVIORS_CLIPBOARD_KIND ); @@ -636,6 +667,9 @@ const BehaviorsEditor = (props: Props) => { onBehaviorsUpdated={onBehaviorsUpdated} onChangeBehaviorName={onChangeBehaviorName} openExtension={openExtension} + openBehaviorPropertiesQuickCustomizationDialog={ + openBehaviorPropertiesQuickCustomizationDialog + } canPasteBehaviors={isClipboardContainingBehaviors} pasteBehaviors={pasteBehaviors} resourceManagementProps={props.resourceManagementProps} @@ -702,6 +736,18 @@ const BehaviorsEditor = (props: Props) => { eventsFunctionsExtension={eventsFunctionsExtension} /> )} + + {!!selectedQuickCustomizationPropertiesBehavior && ( + setSelectedQuickCustomizationPropertiesBehavior(null)} + propertyNames={selectedQuickCustomizationPropertiesBehavior + .getProperties() + .keys() + .toJSArray()} + propertiesQuickCustomizationVisibilities={selectedQuickCustomizationPropertiesBehavior.getPropertiesQuickCustomizationVisibilities()} + /> + )} ); }; diff --git a/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js b/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js index afef9fd88603..4a36d385d1eb 100644 --- a/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js +++ b/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js @@ -164,13 +164,10 @@ const createField = ( const extraInfos = property.getExtraInfo().toJSArray(); // $FlowFixMe - assume the passed resource kind is always valid. const kind: ResourceKind = extraInfos[0] || ''; - // $FlowFixMe - assume the passed resource kind is always valid. - const fallbackKind: ResourceKind = extraInfos[1] || ''; return { name, valueType: 'resource', resourceKind: kind, - fallbackResourceKind: fallbackKind, getValue: (instance: Instance): string => { return getProperties(instance) .get(name) @@ -256,11 +253,17 @@ const uncapitalize = str => { * @param visibility `true` when only deprecated properties must be displayed * and `false` when only not deprecated ones must be displayed */ -const isPropertyVisible = ( +const isPropertyVisible = ({ + properties, + name, + visibility, + quickCustomizationVisibilities, +}: { properties: gdMapStringPropertyDescriptor, name: string, - visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick' -): boolean => { + visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick', + quickCustomizationVisibilities?: gdQuickCustomizationVisibilitiesContainer, +}): boolean => { if (!properties.has(name)) { return false; } @@ -285,7 +288,7 @@ const isPropertyVisible = ( if (property.isDeprecated()) return false; if (property.isAdvanced()) return false; - // Honor visibility if set: + // Honor visibility if set on the property. if ( property.getQuickCustomizationVisibility() === gd.QuickCustomization.Hidden @@ -297,6 +300,13 @@ const isPropertyVisible = ( ) return true; + // Honor visibility if set on the container. + if (quickCustomizationVisibilities) { + const visibility = quickCustomizationVisibilities.get(name); + if (visibility === gd.QuickCustomization.Hidden) return false; + if (visibility === gd.QuickCustomization.Visible) return true; + } + // Otherwise, hide some properties that we know are complex. const propertyType = property.getType(); if (propertyType === 'Behavior') return false; // Hide "required behaviors". @@ -314,7 +324,14 @@ const isPropertyVisible = ( * @param getProperties The function called to read again the properties * @param onUpdateProperty The function called to update a property of an object */ -const propertiesMapToSchema = ( +const propertiesMapToSchema = ({ + properties, + getProperties, + onUpdateProperty, + object, + visibility = 'All', + quickCustomizationVisibilities, +}: { properties: gdMapStringPropertyDescriptor, getProperties: (instance: Instance) => any, onUpdateProperty: ( @@ -322,14 +339,10 @@ const propertiesMapToSchema = ( propertyName: string, newValue: string ) => void, - object: ?gdObject, - visibility: - | 'All' - | 'Basic' - | 'Advanced' - | 'Deprecated' - | 'Basic-Quick' = 'All' -): Schema => { + object?: gdObject, + visibility?: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick', + quickCustomizationVisibilities?: gdQuickCustomizationVisibilitiesContainer, +}): Schema => { const propertyNames = properties.keys(); // Aggregate field by groups to be able to build field groups with a title. const fieldsByGroups = new Map>(); @@ -337,7 +350,14 @@ const propertiesMapToSchema = ( mapFor(0, propertyNames.size(), i => { const name = propertyNames.at(i); const property = properties.get(name); - if (!isPropertyVisible(properties, name, visibility)) { + if ( + !isPropertyVisible({ + properties, + name, + visibility, + quickCustomizationVisibilities, + }) + ) { return null; } if (alreadyHandledProperties.has(name)) return null; @@ -361,7 +381,14 @@ const propertiesMapToSchema = ( name.replace(keyword, otherKeyword) ); for (const rowPropertyName of rowAllPropertyNames) { - if (isPropertyVisible(properties, rowPropertyName, visibility)) { + if ( + isPropertyVisible({ + properties, + name: rowPropertyName, + visibility, + quickCustomizationVisibilities, + }) + ) { rowPropertyNames.push(rowPropertyName); } } @@ -372,7 +399,14 @@ const propertiesMapToSchema = ( name.replace(uncapitalizeKeyword, uncapitalize(otherKeyword)) ); for (const rowPropertyName of rowAllPropertyNames) { - if (isPropertyVisible(properties, rowPropertyName, visibility)) { + if ( + isPropertyVisible({ + properties, + name: rowPropertyName, + visibility, + quickCustomizationVisibilities, + }) + ) { rowPropertyNames.push(rowPropertyName); } } diff --git a/newIDE/app/src/CompactPropertiesEditor/index.js b/newIDE/app/src/CompactPropertiesEditor/index.js index 0955518a95df..5151e656e34c 100644 --- a/newIDE/app/src/CompactPropertiesEditor/index.js +++ b/newIDE/app/src/CompactPropertiesEditor/index.js @@ -116,7 +116,6 @@ export type PrimitiveValueField = export type ResourceField = {| valueType: 'resource', resourceKind: ResourceKind, - fallbackResourceKind?: ResourceKind, getValue: Instance => string, setValue: (instance: Instance, newValue: string) => void, renderLeftIcon?: (className?: string) => React.Node, @@ -229,8 +228,6 @@ const styles = { }, container: { flex: 1, minWidth: 0 }, separator: { - marginRight: -marginsSize, - marginLeft: -marginsSize, marginTop: marginsSize, borderTop: '1px solid black', // Border color is changed in the component. }, @@ -401,21 +398,26 @@ const CompactPropertiesEditor = ({ const { setValue } = field; return ( - { - instances.forEach(i => setValue(i, newValue)); - onFieldChanged({ - instances, - hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, - }); - }} - disabled={getDisabled({ instances, field })} - fullWidth + field={ + { + instances.forEach(i => setValue(i, newValue)); + onFieldChanged({ + instances, + hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, + }); + }} + disabled={getDisabled({ instances, field })} + fullWidth + /> + } /> ); } else if (field.valueType === 'number') { @@ -755,26 +757,29 @@ const CompactPropertiesEditor = ({ const { setValue } = field; return ( - { - instances.forEach(i => setValue(i, newValue)); - onFieldChanged({ - instances, - hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, - }); - }} label={getFieldLabel({ instances, field })} markdownDescription={getFieldDescription(field)} + field={ + { + instances.forEach(i => setValue(i, newValue)); + onFieldChanged({ + instances, + hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, + }); + }} + /> + } /> ); }; diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/OptionsEditorDialog/ExtensionOptionsEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/OptionsEditorDialog/ExtensionOptionsEditor.js index ae0232e4c76d..4ebbeec2362e 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/OptionsEditorDialog/ExtensionOptionsEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/OptionsEditorDialog/ExtensionOptionsEditor.js @@ -118,7 +118,7 @@ export const ExtensionOptionsEditor = ({ return ( {({ i18n }: { i18n: I18nType }) => ( - + Name} value={eventsFunctionsExtension.getName()} diff --git a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js index e3e4a4a10b76..a495edc638c6 100644 --- a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js +++ b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js @@ -46,6 +46,7 @@ type OnlineGameLinkProps = {| exportStep: BuildStep, onRefreshGame: () => Promise, automaticallyOpenGameProperties?: boolean, + shouldShowShareDialog: boolean, |}; const timeForExport = 5; // seconds. @@ -60,6 +61,7 @@ const OnlineGameLink = ({ exportStep, onRefreshGame, automaticallyOpenGameProperties, + shouldShowShareDialog, }: OnlineGameLinkProps) => { const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState( false @@ -177,10 +179,12 @@ const OnlineGameLink = ({ () => { if (exportStep === 'done') { setTimeBeforeExportFinished(timeForExport); // reset. - setIsShareDialogOpen(true); + if (shouldShowShareDialog) { + setIsShareDialogOpen(true); + } } }, - [exportStep, automaticallyOpenGameProperties] + [exportStep, automaticallyOpenGameProperties, shouldShowShareDialog] ); React.useEffect( diff --git a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js index 5eff2421714d..cfb4da4214ab 100644 --- a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js +++ b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js @@ -105,6 +105,7 @@ const OnlineWebExportFlow = ({ automaticallyOpenGameProperties={automaticallyOpenGameProperties} onRefreshGame={refreshGame} game={game} + shouldShowShareDialog={uiMode === 'full'} /> ); diff --git a/newIDE/app/src/GameDashboard/ShareGameDialog.js b/newIDE/app/src/GameDashboard/ShareGameDialog.js index 19f04b614811..5f1cb2fe8801 100644 --- a/newIDE/app/src/GameDashboard/ShareGameDialog.js +++ b/newIDE/app/src/GameDashboard/ShareGameDialog.js @@ -14,7 +14,7 @@ import ShareButton from '../UI/ShareDialog/ShareButton'; type Props = {| game: Game, onClose: () => void |}; -const ShareDialog = ({ game, onClose }: Props) => { +const ShareGameDialog = ({ game, onClose }: Props) => { const [showAlertMessage, setShowAlertMessage] = React.useState(false); const gameUrl = getGameUrl(game); @@ -55,4 +55,4 @@ const ShareDialog = ({ game, onClose }: Props) => { ); }; -export default ShareDialog; +export default ShareGameDialog; diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js index 2d68fb3d7050..142eacc0c4bf 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js @@ -138,21 +138,21 @@ const CompactInstancePropertiesEditor = ({ const canBeFlippedZ = objectMetadata.hasDefaultBehavior( 'Scene3D::Base3DBehavior' ); - const instanceSchemaForCustomProperties = propertiesMapToSchema( + const instanceSchemaForCustomProperties = propertiesMapToSchema({ properties, - (instance: gdInitialInstance) => + getProperties: (instance: gdInitialInstance) => instance.getCustomProperties( globalObjectsContainer || objectsContainer, objectsContainer ), - (instance: gdInitialInstance, name, value) => + onUpdateProperty: (instance: gdInitialInstance, name, value) => instance.updateCustomProperty( name, value, globalObjectsContainer || objectsContainer, objectsContainer - ) - ); + ), + }); const reorderedInstanceSchemaForCustomProperties = reorderInstanceSchemaForCustomProperties( instanceSchemaForCustomProperties, diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index cfccfbef45a2..437a7d7e1baf 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -58,6 +58,8 @@ const isShopRequested = (routeArguments: RouteArguments): boolean => routeArguments['initial-dialog'] === 'store'; // New way of opening the store const isGamesDashboardRequested = (routeArguments: RouteArguments): boolean => routeArguments['initial-dialog'] === 'games-dashboard'; +const isBuildRequested = (routeArguments: RouteArguments): boolean => + routeArguments['initial-dialog'] === 'build'; const styles = { container: { @@ -220,10 +222,15 @@ export const HomePage = React.memo( const isGamesDashboardRequestedAtOpening = React.useRef( isGamesDashboardRequested(routeArguments) ); + const isBuildRequestedAtOpening = React.useRef( + isBuildRequested(routeArguments) + ); const initialTab = isShopRequestedAtOpening.current ? 'shop' : isGamesDashboardRequestedAtOpening.current ? 'manage' + : isBuildRequestedAtOpening.current + ? 'build' : showGetStartedSectionByDefault ? 'get-started' : 'build'; @@ -298,6 +305,9 @@ export const HomePage = React.memo( } else if (isGamesDashboardRequested(routeArguments)) { setActiveTab('manage'); removeRouteArguments(['initial-dialog']); + } else if (isBuildRequested(routeArguments)) { + setActiveTab('build'); + removeRouteArguments(['initial-dialog']); } }, [ diff --git a/newIDE/app/src/MainFrame/RouterContext.js b/newIDE/app/src/MainFrame/RouterContext.js index b7897eeb3afd..601dc64905ca 100644 --- a/newIDE/app/src/MainFrame/RouterContext.js +++ b/newIDE/app/src/MainFrame/RouterContext.js @@ -8,7 +8,8 @@ export type Route = | 'subscription' | 'games-dashboard' | 'asset-store' // For compatibility when there was only asset packs. - | 'store'; // New way of opening the store. + | 'store' // New way of opening the store. + | 'build'; type RouteKey = | 'initial-dialog' | 'game-id' diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 919b7f36eb10..25ff867cdcc8 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -177,6 +177,7 @@ import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders import CustomDragLayer from '../UI/DragAndDrop/CustomDragLayer'; import CloudProjectRecoveryDialog from '../ProjectsStorage/CloudStorageProvider/CloudProjectRecoveryDialog'; import CloudProjectSaveChoiceDialog from '../ProjectsStorage/CloudStorageProvider/CloudProjectSaveChoiceDialog'; +import CloudStorageProvider from '../ProjectsStorage/CloudStorageProvider'; import useCreateProject from '../Utils/UseCreateProject'; import newNameGenerator from '../Utils/NewNameGenerator'; import { addDefaultLightToAllLayers } from '../ProjectCreation/CreateProject'; @@ -193,6 +194,7 @@ import { useAuthenticatedPlayer } from './UseAuthenticatedPlayer'; import ListIcon from '../UI/ListIcon'; import { QuickCustomizationDialog } from '../QuickCustomization/QuickCustomizationDialog'; import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects'; +import RouterContext from './RouterContext'; const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || []; @@ -381,6 +383,7 @@ const MainFrame = (props: Props) => { newProjectSetupDialogOpen, setNewProjectSetupDialogOpen, ] = React.useState(false); + const { navigateToRoute } = React.useContext(RouterContext); const [isProjectOpening, setIsProjectOpening] = React.useState( false @@ -2409,7 +2412,12 @@ const MainFrame = (props: Props) => { ); const saveProjectAsWithStorageProvider = React.useCallback( - async (requestedStorageProvider?: StorageProvider) => { + async ( + options: ?{| + requestedStorageProvider?: StorageProvider, + forcedSavedAsLocation?: SaveAsLocation, + |} + ) => { if (!currentProject) return; saveUiSettings(state.editorTabs); @@ -2427,6 +2435,8 @@ const MainFrame = (props: Props) => { const oldStorageProviderOperations = getStorageProviderOperations(); // Get the methods to save the project using the *new* storage provider. + const requestedStorageProvider = + options && options.requestedStorageProvider; const newStorageProviderOperations = getStorageProviderOperations( requestedStorageProvider ); @@ -2452,8 +2462,9 @@ const MainFrame = (props: Props) => { const storageProviderInternalName = newStorageProvider.internalName; try { - let newSaveAsLocation: ?SaveAsLocation = null; - if (onChooseSaveProjectAsLocation) { + let newSaveAsLocation: ?SaveAsLocation = + options && options.forcedSavedAsLocation; + if (onChooseSaveProjectAsLocation && !newSaveAsLocation) { const { saveAsLocation } = await onChooseSaveProjectAsLocation({ project: currentProject, fileMetadata: currentFileMetadata, @@ -2461,19 +2472,19 @@ const MainFrame = (props: Props) => { if (!saveAsLocation) { return; // Save as was cancelled. } + newSaveAsLocation = saveAsLocation; + } - if (canFileMetadataBeSafelySavedAs && currentFileMetadata) { - const canProjectBeSafelySavedAs = await canFileMetadataBeSafelySavedAs( - currentFileMetadata, - { - showAlert, - showConfirmation, - } - ); + if (canFileMetadataBeSafelySavedAs && currentFileMetadata) { + const canProjectBeSafelySavedAs = await canFileMetadataBeSafelySavedAs( + currentFileMetadata, + { + showAlert, + showConfirmation, + } + ); - if (!canProjectBeSafelySavedAs) return; - } - newSaveAsLocation = saveAsLocation; + if (!canProjectBeSafelySavedAs) return; } const { wasSaved, fileMetadata } = await onSaveProjectAs( @@ -3765,7 +3776,9 @@ const MainFrame = (props: Props) => { storageProviders={props.storageProviders} onChooseProvider={storageProvider => { openSaveToStorageProviderDialog(false); - saveProjectAsWithStorageProvider(storageProvider); + saveProjectAsWithStorageProvider({ + requestedStorageProvider: storageProvider, + }); }} /> )} @@ -3867,17 +3880,50 @@ const MainFrame = (props: Props) => { onLaunchPreview={ hotReloadPreviewButtonProps.launchProjectDataOnlyPreview } - onClose={options => { + onClose={async options => { + if (hasUnsavedChanges) { + const response = await showConfirmation({ + title: t`Leave the customization?`, + message: t`Do you want to quit the customization? All your changes will be lost.`, + confirmButtonLabel: t`Leave`, + }); + + if (!response) { + return; + } + } + setQuickCustomizationDialogOpenedFromGameId(null); - if (options && options.tryAnotherGame) { - // Close the project so the user is back at where they can chose a game to customize - // which is probably the home page. - closeProject(); - openHomePage(); + closeProject(); + openHomePage(); + if (!hasUnsavedChanges) { + navigateToRoute('build'); } }} onlineWebExporter={quickPublishOnlineWebExporter} - onSaveProject={saveProject} + onSaveProject={async () => { + // Automatically try to save project to the cloud. + const storageProvider = getStorageProvider(); + if ( + !['Empty', 'UrlStorageProvider', 'Cloud'].includes( + storageProvider.internalName + ) + ) { + console.error( + `Unexpected storage provider ${ + storageProvider.internalName + } when saving project from quick customization dialog. Saving anyway.` + ); + } + + saveProjectAsWithStorageProvider({ + requestedStorageProvider: CloudStorageProvider, + forcedSavedAsLocation: { + name: currentProject.getName(), + }, + }); + return; + }} isSavingProject={isSavingProject} canClose={true} sourceGameId={quickCustomizationDialogOpenedFromGameId} diff --git a/newIDE/app/src/Profile/AuthenticatedUserProvider.js b/newIDE/app/src/Profile/AuthenticatedUserProvider.js index e909c16c8b45..d77a4a699e50 100644 --- a/newIDE/app/src/Profile/AuthenticatedUserProvider.js +++ b/newIDE/app/src/Profile/AuthenticatedUserProvider.js @@ -309,9 +309,11 @@ export default class AuthenticatedUserProvider extends React.Component< } }; - _fetchUserProfileWithoutThrowingErrors = async () => { + _fetchUserProfileWithoutThrowingErrors = async ( + options: ?{ dontNotifyAboutEmailVerification?: boolean } + ) => { try { - await this._fetchUserProfile(); + await this._fetchUserProfile(options); } catch (error) { console.error( 'Error while fetching the user profile - but ignoring it.', @@ -320,7 +322,9 @@ export default class AuthenticatedUserProvider extends React.Component< } }; - _fetchUserProfile = async () => { + _fetchUserProfile = async ( + options: ?{ dontNotifyAboutEmailVerification?: boolean } + ) => { const { authentication } = this.props; this.setState(({ authenticatedUser }) => ({ @@ -561,7 +565,9 @@ export default class AuthenticatedUserProvider extends React.Component< // We call this function every time the user is fetched, as it will // automatically prevent the event to be sent if the user attributes haven't changed. identifyUserForAnalytics(this.state.authenticatedUser); - this._notifyUserAboutEmailVerification(); + if (!options || !options.dontNotifyAboutEmailVerification) { + this._notifyUserAboutEmailVerification(); + } } ); }; @@ -919,6 +925,8 @@ export default class AuthenticatedUserProvider extends React.Component< if (!authentication) return; this.setState({ + // This function is used for both account creation & login. + createAccountInProgress: true, loginInProgress: true, apiCallError: null, authenticatedUser: { @@ -956,6 +964,7 @@ export default class AuthenticatedUserProvider extends React.Component< } } this.setState({ + createAccountInProgress: false, loginInProgress: false, authenticatedUser: { ...this.state.authenticatedUser, @@ -1083,7 +1092,10 @@ export default class AuthenticatedUserProvider extends React.Component< // by the API when fetched. } - await this._fetchUserProfileWithoutThrowingErrors(); + await this._fetchUserProfileWithoutThrowingErrors({ + // When creating an account, avoid showing the email verification dialog right away. + dontNotifyAboutEmailVerification: true, + }); this.openCreateAccountDialog(false); sendSignupDone(form.email); const firebaseUser = this.state.authenticatedUser.firebaseUser; diff --git a/newIDE/app/src/QuickCustomization/GameImage.js b/newIDE/app/src/QuickCustomization/GameImage.js new file mode 100644 index 000000000000..88ee3a98c161 --- /dev/null +++ b/newIDE/app/src/QuickCustomization/GameImage.js @@ -0,0 +1,46 @@ +// @flow +import * as React from 'react'; + +const styles = { + image: { + width: '100%', + maxHeight: '325px', + objectFit: 'contain', + borderRadius: '16px', + boxSizing: 'border-box', + aspectRatio: '16 / 9', + }, +}; + +type Props = {| + project: gdProject, +|}; + +const GameImage = ({ project }: Props) => { + const rocketUrl = 'res/quick_customization/quick_publish.svg'; + + const gameThumbnailUrl = React.useMemo( + () => { + const resourcesManager = project.getResourcesManager(); + const thumbnailName = project + .getPlatformSpecificAssets() + .get('liluo', `thumbnail`); + if (!thumbnailName) return rocketUrl; + const path = resourcesManager.getResource(thumbnailName).getFile(); + if (!path) return rocketUrl; + + return path; + }, + [project] + ); + + return ( + Customize your game with GDevelop + ); +}; + +export default GameImage; diff --git a/newIDE/app/src/QuickCustomization/PreviewLine.js b/newIDE/app/src/QuickCustomization/PreviewLine.js new file mode 100644 index 000000000000..4801330707f8 --- /dev/null +++ b/newIDE/app/src/QuickCustomization/PreviewLine.js @@ -0,0 +1,60 @@ +// @flow +import * as React from 'react'; +import { Trans } from '@lingui/macro'; +import FlatButton from '../UI/FlatButton'; +import { LineStackLayout } from '../UI/Layout'; +import Text from '../UI/Text'; +import PreviewIcon from '../UI/CustomSvgIcons/Preview'; +import { Column } from '../UI/Grid'; +import Paper from '../UI/Paper'; +import PlaySquared from '../UI/CustomSvgIcons/PlaySquared'; +import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; +import classes from './PreviewLine.module.css'; +import classNames from 'classnames'; + +type Props = {| + onLaunchPreview: () => Promise, +|}; + +const PreviewLine = ({ onLaunchPreview }: Props) => { + const gdevelopTheme = React.useContext(GDevelopThemeContext); + + return ( + + +
+ + + + + + Preview your game + + + Preview} + onClick={onLaunchPreview} + leftIcon={} + /> + + +
+
+
+ ); +}; + +export default PreviewLine; diff --git a/newIDE/app/src/QuickCustomization/PreviewLine.module.css b/newIDE/app/src/QuickCustomization/PreviewLine.module.css new file mode 100644 index 000000000000..abd68a72449b --- /dev/null +++ b/newIDE/app/src/QuickCustomization/PreviewLine.module.css @@ -0,0 +1,5 @@ +.previewLine { + border-left: 4px solid var(--theme-message-valid-color); + border-radius: 6px; +} + diff --git a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js index 3ba736acd64a..2979107e1f8c 100644 --- a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js +++ b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js @@ -9,9 +9,9 @@ import { enumerateObjectFolderOrObjects } from '.'; import CompactPropertiesEditor from '../CompactPropertiesEditor'; import propertiesMapToSchema from '../CompactPropertiesEditor/PropertiesMapToCompactSchema'; import { Trans } from '@lingui/macro'; -import { CalloutCard } from '../UI/CalloutCard'; -import { LargeSpacer } from '../UI/Grid'; import { useForceRecompute } from '../Utils/UseForceUpdate'; +import TipCard from './TipCard'; +import { Column } from '../UI/Grid'; const gd: libGDevelop = global.gd; @@ -34,28 +34,31 @@ const QuickBehaviorPropertiesEditor = ({ if (schemaRecomputeTrigger) { // schemaRecomputeTrigger allows to invalidate the schema when required. } - return propertiesMapToSchema( - behavior.getProperties(), - behavior => behavior.getProperties(), - (behavior, name, value) => { + return propertiesMapToSchema({ + properties: behavior.getProperties(), + getProperties: behavior => behavior.getProperties(), + onUpdateProperty: (behavior, name, value) => { behavior.updateProperty(name, value); }, object, - 'Basic-Quick' - ); + visibility: 'Basic-Quick', + quickCustomizationVisibilities: behavior.getPropertiesQuickCustomizationVisibilities(), + }); }, [behavior, object, schemaRecomputeTrigger] ); return ( - + + + ); }; @@ -106,6 +109,15 @@ export const QuickBehaviorsTweaker = ({ }: Props) => { return ( + These are behaviors} + description={ + + Behaviors are attached to objects and make them alive. The rules of + the game can be created using behaviors and events. + + } + /> {mapFor(0, project.getLayoutsCount(), i => { const layout = project.getLayoutAt(i); const folderObjects = enumerateObjectFolderOrObjects( @@ -126,7 +138,7 @@ export const QuickBehaviorsTweaker = ({ } return ( - + {behaviorNamesToTweak.map(behaviorName => { @@ -139,6 +151,7 @@ export const QuickBehaviorsTweaker = ({ object={object} onBehaviorUpdated={() => {}} resourceManagementProps={resourceManagementProps} + key={behavior.ptr} /> ); })} @@ -153,7 +166,12 @@ export const QuickBehaviorsTweaker = ({ } return ( - + {folderName} @@ -175,7 +193,12 @@ export const QuickBehaviorsTweaker = ({ } return ( - + {project.getLayoutsCount() > 1 && ( {layout.getName()} @@ -185,30 +208,6 @@ export const QuickBehaviorsTweaker = ({ ); }).filter(Boolean)} - - ( - - )} - > - - - - Making a fun game with behaviors - - - - Behaviors are attached to objects and make them alive. The rules - of the game can be created using behaviors and events. - - - - - ); }; diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js b/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js index 71123167a35c..e689d3285cc8 100644 --- a/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js @@ -5,20 +5,19 @@ import { renderQuickCustomization, useQuickCustomizationState } from '.'; import { Trans } from '@lingui/macro'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import FlatButton from '../UI/FlatButton'; -import { ColumnStackLayout, LineStackLayout } from '../UI/Layout'; -import Text from '../UI/Text'; -import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; -import Paper from '../UI/Paper'; +import { ColumnStackLayout } from '../UI/Layout'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import { useGameAndBuildsManager } from '../Utils/UseGameAndBuildsManager'; import { sendQuickCustomizationProgress } from '../Utils/Analytics/EventSender'; import ScrollView from '../UI/ScrollView'; +import PreviewLine from './PreviewLine'; +import UnsavedChangesContext from '../MainFrame/UnsavedChangesContext'; type Props = {| project: gdProject, resourceManagementProps: ResourceManagementProps, onLaunchPreview: () => Promise, - onClose: (?{| tryAnotherGame: boolean |}) => void, + onClose: (?{| tryAnotherGame: boolean |}) => Promise, onlineWebExporter: Exporter, onSaveProject: () => Promise, isSavingProject: boolean, @@ -37,14 +36,12 @@ export const QuickCustomizationDialog = ({ canClose, sourceGameId, }: Props) => { + const { triggerUnsavedChanges } = React.useContext(UnsavedChangesContext); const gameAndBuildsManager = useGameAndBuildsManager({ project, copyLeaderboardsAndMultiplayerLobbiesFromGameId: sourceGameId, }); const quickCustomizationState = useQuickCustomizationState({ onClose }); - const { windowSize } = useResponsiveWindowSize(); - - const isMediumOrSmaller = windowSize === 'small' || windowSize === 'medium'; const onContinueQuickCustomization = React.useCallback( () => { @@ -60,12 +57,7 @@ export const QuickCustomizationDialog = ({ [onClose] ); - const { - title, - titleRightContent, - titleTopContent, - content, - } = renderQuickCustomization({ + const { title, content, showPreview } = renderQuickCustomization({ project, gameAndBuildsManager, resourceManagementProps, @@ -91,70 +83,64 @@ export const QuickCustomizationDialog = ({ [quickCustomizationState.step.name, sourceGameId, name] ); + React.useEffect( + () => { + triggerUnsavedChanges(); + }, + // Trigger unsaved changes when the dialog is opened, + // so the user is warned if they try to close the dialog. + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return ( {titleTopContent} - ) : null - } + title={title} maxWidth="md" fullHeight actions={ - !quickCustomizationState.step.shouldHideNavigationButtons + !!quickCustomizationState.step.nextLabel ? [ - quickCustomizationState.canGoToPreviousStep ? ( - Back} - onClick={quickCustomizationState.goToPreviousStep} - disabled={ - !quickCustomizationState.canGoToPreviousStep || - quickCustomizationState.isNavigationDisabled - } - /> - ) : null, , ] - : undefined + : [] } secondaryActions={[ - quickCustomizationState.step.shouldHideNavigationButtons || - !canClose ? null : ( + !quickCustomizationState.canGoToPreviousStep ? null : ( Close} - primary={false} - onClick={onClose} - disabled={quickCustomizationState.isNavigationDisabled} + key="previous" + primary + label={Back} + onClick={quickCustomizationState.goToPreviousStep} + disabled={ + !quickCustomizationState.canGoToPreviousStep || + quickCustomizationState.isNavigationDisabled + } + fullWidth /> ), ]} + onRequestClose={canClose ? onClose : undefined} flexBody + actionsFullWidthOnMobile + cannotBeDismissed={quickCustomizationState.isNavigationDisabled} > - - - - - {title} - - {!isMediumOrSmaller ? titleRightContent : null} - - {content} - - + + + + {content} + + + {showPreview && } + ); }; diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js b/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js index a2937467ff68..2d97a2a78e63 100644 --- a/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js @@ -78,6 +78,7 @@ export const QuickCustomizationGameTiles = ({ thumbnailTitleByLocale )} key={exampleShortHeader.name} + useQuickCustomizationThumbnail /> ) ) diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js b/newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js new file mode 100644 index 000000000000..7052bd0b66a3 --- /dev/null +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js @@ -0,0 +1,114 @@ +// @flow +import { t, Trans } from '@lingui/macro'; + +import * as React from 'react'; +import Dialog from '../UI/Dialog'; +import FlatButton from '../UI/FlatButton'; +import { mapFor } from '../Utils/MapFor'; +import Text from '../UI/Text'; +import SelectField from '../UI/SelectField'; +import SelectOption from '../UI/SelectOption'; +import { LineStackLayout } from '../UI/Layout'; +import useForceUpdate from '../Utils/UseForceUpdate'; + +const gd: libGDevelop = global.gd; + +const getVisibilityForProperty = ( + propertyName: string, + propertiesQuickCustomizationVisibilities: gdQuickCustomizationVisibilitiesContainer +) => { + return propertiesQuickCustomizationVisibilities.get(propertyName); +}; + +type Props = {| + propertyNames: string[], + open: boolean, + onClose: () => void, + propertiesQuickCustomizationVisibilities: gdQuickCustomizationVisibilitiesContainer, +|}; + +export default function QuickCustomizationPropertiesVisibilityDialog({ + open, + onClose, + propertyNames, + propertiesQuickCustomizationVisibilities, +}: Props) { + const forceUpdate = useForceUpdate(); + + return ( + Quick Customization: Behavior properties} + secondaryActions={[ + Close} + primary={false} + onClick={onClose} + />, + ]} + open + onRequestClose={onClose} + flexColumnBody + fullHeight + > + {mapFor(0, propertyNames.length, i => { + const propertyName = propertyNames[i]; + const value = getVisibilityForProperty( + propertyName, + propertiesQuickCustomizationVisibilities + ); + + return ( + + {propertyName} + { + const newQuickCustomizationVisibility = parseInt(newValue, 10); + if ( + [ + gd.QuickCustomization.Visible, + gd.QuickCustomization.Hidden, + gd.QuickCustomization.Default, + ].includes(newQuickCustomizationVisibility) + ) { + propertiesQuickCustomizationVisibilities.set( + propertyName, + // $FlowIgnore: We checked that newQuickCustomizationVisibility is a valid visibility + newQuickCustomizationVisibility + ); + forceUpdate(); + return; + } + + propertiesQuickCustomizationVisibilities.set( + propertyName, + gd.QuickCustomization.Default + ); + forceUpdate(); + }} + > + + + + + + ); + })} + + ); +} diff --git a/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js b/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js index 9981071f2cd6..6d852204af07 100644 --- a/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js +++ b/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js @@ -2,15 +2,14 @@ import * as React from 'react'; import { ObjectPreview } from './ObjectPreview'; import { mapFor } from '../Utils/MapFor'; -import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout'; +import { ColumnStackLayout } from '../UI/Layout'; import FlatButton from '../UI/FlatButton'; import AssetSwappingDialog from '../AssetStore/AssetSwappingDialog'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import Text from '../UI/Text'; import { Trans } from '@lingui/macro'; import { enumerateObjectFolderOrObjects } from '.'; -import { CalloutCard } from '../UI/CalloutCard'; -import { LargeSpacer } from '../UI/Grid'; +import TipCard from './TipCard'; type Props = {| project: gdProject, @@ -35,6 +34,15 @@ export const QuickObjectReplacer = ({ return ( + These are objects} + description={ + + Each character, player, obstacle, background, item, etc. is an + object. Objects are the building blocks of your game. + + } + /> {mapFor(0, project.getLayoutsCount(), i => { const layout = project.getLayoutAt(i); const folderObjects = enumerateObjectFolderOrObjects( @@ -44,7 +52,7 @@ export const QuickObjectReplacer = ({ if (!folderObjects.length) return null; return ( - + {project.getLayoutsCount() > 1 && ( {layout.getName()} @@ -52,13 +60,13 @@ export const QuickObjectReplacer = ({ )} {folderObjects.map(({ folderName, objects }) => { return ( - + {folderName}
{objects.map(object => ( - + ); })} - - ( - - )} - > - - - - Objects are everything in your game - - - - Each character, player, obstacle, background, item, etc. is an - object. Objects are the building blocks of your game. - - - - - {selectedObjectToSwap && ( { setSelectedObjectToSwap(null); }} + minimalUI /> )} diff --git a/newIDE/app/src/QuickCustomization/QuickPublish.js b/newIDE/app/src/QuickCustomization/QuickPublish.js index dc6d0dc456eb..74d8429e0551 100644 --- a/newIDE/app/src/QuickCustomization/QuickPublish.js +++ b/newIDE/app/src/QuickCustomization/QuickPublish.js @@ -4,27 +4,34 @@ import { Trans } from '@lingui/macro'; import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext'; import ExportLauncher from '../ExportAndShare/ShareDialog/ExportLauncher'; import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext'; -import { ColumnStackLayout } from '../UI/Layout'; +import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout'; import RaisedButton from '../UI/RaisedButton'; import { I18n } from '@lingui/react'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import Text from '../UI/Text'; import { type GameAndBuildsManager } from '../Utils/UseGameAndBuildsManager'; import FlatButton from '../UI/FlatButton'; -import { Spacer } from '../UI/Grid'; -import TextButton from '../UI/TextButton'; +import { Column, Line, Spacer } from '../UI/Grid'; import classes from './QuickPublish.module.css'; import classNames from 'classnames'; +import Paper from '../UI/Paper'; +import Google from '../UI/CustomSvgIcons/Google'; +import GitHub from '../UI/CustomSvgIcons/GitHub'; +import Apple from '../UI/CustomSvgIcons/Apple'; +import TextButton from '../UI/TextButton'; +import Trash from '../UI/CustomSvgIcons/Trash'; +import GameImage from './GameImage'; +import ShareLink from '../UI/ShareDialog/ShareLink'; +import { getGameUrl } from '../Utils/GDevelopServices/Game'; type Props = {| project: gdProject, gameAndBuildsManager: GameAndBuildsManager, setIsNavigationDisabled: (isNavigationDisabled: boolean) => void, - shouldAutomaticallyStartExport: boolean, onlineWebExporter: Exporter, onSaveProject: () => Promise, isSavingProject: boolean, - onClose: () => void, + onClose: () => Promise, onContinueQuickCustomization: () => void, onTryAnotherGame: () => void, |}; @@ -33,7 +40,6 @@ export const QuickPublish = ({ project, gameAndBuildsManager, setIsNavigationDisabled, - shouldAutomaticallyStartExport, onlineWebExporter, onSaveProject, isSavingProject, @@ -43,6 +49,7 @@ export const QuickPublish = ({ }: Props) => { const authenticatedUser = React.useContext(AuthenticatedUserContext); const { profile, onOpenCreateAccountDialog } = authenticatedUser; + const { game } = gameAndBuildsManager; const eventsFunctionsExtensionsState = React.useContext( EventsFunctionsExtensionsContext ); @@ -61,129 +68,162 @@ export const QuickPublish = ({ React.useEffect( () => { - if (profile && shouldAutomaticallyStartExport) { + if (profile && exportState === '') { + // Save project & launch export as soon as the user is authenticated (or if they already were) + onSaveProject(); launchExport(); } }, - [profile, shouldAutomaticallyStartExport, launchExport] + [profile, launchExport, onSaveProject, exportState] ); + const gameUrl = game ? getGameUrl(game) : ''; + const hasNotSavedProject = !profile && exportState === ''; + return ( - - - Publish your game with GDevelop - {profile ? ( - - {({ i18n }) => ( - - { - // Nothing to do. - }} - authenticatedUser={authenticatedUser} - eventsFunctionsExtensionsState={eventsFunctionsExtensionsState} - exportPipeline={onlineWebExporter.exportPipeline} - setIsNavigationDisabled={setIsNavigationDisabled} - gameAndBuildsManager={gameAndBuildsManager} - uiMode="minimal" - onExportLaunched={() => { - setExportState('started'); - }} - onExportErrored={() => { - setExportState('errored'); - }} - onExportSucceeded={() => { - setExportState('succeeded'); - }} - /> - {exportState === 'succeeded' ? ( - - - Congratulations! Your game is now published. - - Continue tweaking the game} - onClick={onContinueQuickCustomization} - /> - Edit the full game} - onClick={onClose} - /> - - or - - Try with another game} - onClick={onTryAnotherGame} - /> - - ) : !shouldAutomaticallyStartExport && exportState === '' ? ( + + + + + {profile ? ( + + {({ i18n }) => ( + + { + // Nothing to do. + }} + authenticatedUser={authenticatedUser} + eventsFunctionsExtensionsState={ + eventsFunctionsExtensionsState + } + exportPipeline={onlineWebExporter.exportPipeline} + setIsNavigationDisabled={setIsNavigationDisabled} + gameAndBuildsManager={gameAndBuildsManager} + uiMode="minimal" + onExportLaunched={() => { + setExportState('started'); + }} + onExportErrored={() => { + setExportState('errored'); + }} + onExportSucceeded={() => { + setExportState('succeeded'); + }} + /> + {exportState === 'succeeded' ? ( + +
+ + + Share your game with your friends! + + {gameUrl && } + +
+
+ ) : exportState === 'errored' ? ( + + + + An error occurred while exporting your game. Verify your + internet connection and try again. + + + Try again} + onClick={launchExport} + /> + + ) : null} +
+ )} +
+ ) : ( + + +
- Go back and tweak the game} - onClick={onContinueQuickCustomization} - /> - - or - - Edit the full game} - onClick={onClose} - /> - - ) : exportState === 'errored' ? ( - - An error occurred while exporting your game. Verify your - internet connection and try again. + Create a GDevelop account to save your changes and keep + personalizing your game - Try again} - onClick={launchExport} - /> + + } + label={Google} + onClick={onOpenCreateAccountDialog} + fullWidth + /> + } + label={Github} + onClick={onOpenCreateAccountDialog} + fullWidth + /> + } + label={Apple} + onClick={onOpenCreateAccountDialog} + fullWidth + /> + Edit the full game} - onClick={onClose} + primary + label={Use your email} + onClick={onOpenCreateAccountDialog} /> - ) : null} - - )} - - ) : ( - - - - Create a GDevelop account to share your game in a few seconds. - - - Create an account} - onClick={onOpenCreateAccountDialog} - keyboardFocused - /> +
+
+
+ )} +
+ + {exportState !== 'started' && ( + Skip and edit the full game} + secondary onClick={onClose} + label={ + hasNotSavedProject ? ( + Leave and lose all changes + ) : ( + Finish and close + ) + } + icon={hasNotSavedProject ? : null} /> -
+ )}
); diff --git a/newIDE/app/src/QuickCustomization/QuickPublish.module.css b/newIDE/app/src/QuickCustomization/QuickPublish.module.css index 5b93d5c32fc0..55a42b865c9d 100644 --- a/newIDE/app/src/QuickCustomization/QuickPublish.module.css +++ b/newIDE/app/src/QuickCustomization/QuickPublish.module.css @@ -1,20 +1,3 @@ -.illustrationImage { - width: 100px; - aspect-ratio: 117 / 162; -} - -.animatedRocket { - animation: animatedRocket ease-in-out 1.2s infinite; -} - -@keyframes animatedRocket { - 0% { transform: rotate(0deg); } - 15% { transform: rotate(4deg); } - 25% { transform: rotate(5deg); } - 35% { transform: rotate(4deg); } - 50% { transform: rotate(0deg); } - 65% { transform: rotate(-4deg); } - 75% { transform: rotate(-5deg); } - 85% { transform: rotate(-4deg); } - 100% { transform: rotate(0deg); } -} +.paperContainer { + padding: 16px; +} \ No newline at end of file diff --git a/newIDE/app/src/QuickCustomization/QuickTitleTweaker.js b/newIDE/app/src/QuickCustomization/QuickTitleTweaker.js new file mode 100644 index 000000000000..75e414a888cb --- /dev/null +++ b/newIDE/app/src/QuickCustomization/QuickTitleTweaker.js @@ -0,0 +1,188 @@ +// @flow +import * as React from 'react'; +import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout'; +import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; +import CompactPropertiesEditor from '../CompactPropertiesEditor'; +import propertiesMapToSchema from '../CompactPropertiesEditor/PropertiesMapToCompactSchema'; +import { useForceRecompute } from '../Utils/UseForceUpdate'; +import { Column, Line } from '../UI/Grid'; +import GameImage from './GameImage'; +import Text from '../UI/Text'; +import { Trans } from '@lingui/macro'; + +const gd: libGDevelop = global.gd; + +export const findTitleObject = ( + objectFolderOrObject: gdObjectFolderOrObject +): ?gdObject => { + for (let i = 0; i < objectFolderOrObject.getChildrenCount(); i++) { + const child = objectFolderOrObject.getChildAt(i); + + if (child.isFolder()) { + const foundTitleObject = findTitleObject(child); + if (foundTitleObject) { + return foundTitleObject; + } + } else { + const object = child.getObject(); + if (object.getName() === 'Title') { + return object; + } + } + } + + return null; +}; + +const QuickObjectPropertiesEditor = ({ + project, + object, + objectConfiguration, + onObjectUpdated, + resourceManagementProps, +}: {| + project: gdProject, + object: gdObject, + objectConfiguration: gdObjectConfiguration, + onObjectUpdated: () => void, + resourceManagementProps: ResourceManagementProps, +|}) => { + const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute(); + + // Properties: + const basicPropertiesSchema = React.useMemo( + () => { + if (schemaRecomputeTrigger) { + // schemaRecomputeTrigger allows to invalidate the schema when required. + } + const properties = objectConfiguration.getProperties(); + const schema = propertiesMapToSchema({ + properties, + getProperties: object => object.getProperties(), + onUpdateProperty: (object, name, value) => + object.updateProperty(name, value), + object, + visibility: 'Basic-Quick', + }); + + return schema; + }, + [objectConfiguration, schemaRecomputeTrigger, object] + ); + + return ( + + + + + + + ); +}; + +type Props = {| + project: gdProject, + resourceManagementProps: ResourceManagementProps, +|}; + +export const QuickTitleTweaker = ({ + project, + resourceManagementProps, +}: Props) => { + const titleObject = React.useMemo( + () => { + for (let i = 0; i < project.getLayoutsCount(); i++) { + const layout = project.getLayoutAt(i); + const titleObject = findTitleObject( + layout.getObjects().getRootFolder() + ); + + if (titleObject) { + return titleObject; + } + } + + return null; + }, + [project] + ); + + const titleObjectConfiguration = React.useMemo( + () => { + if (!titleObject) { + return null; + } + + const objectConfiguration = titleObject.getConfiguration(); + // TODO: Workaround a bad design of ObjectJsImplementation. When getProperties + // and associated methods are redefined in JS, they have different arguments ( + // see ObjectJsImplementation C++ implementation). If called directly here from JS, + // the arguments will be mismatched. To workaround this, always cast the object to + // a base gdObject to ensure C++ methods are called. + const objectConfigurationAsGd = gd.castObject( + objectConfiguration, + gd.ObjectConfiguration + ); + + return objectConfigurationAsGd; + }, + [titleObject] + ); + + const updateProjectName = React.useCallback( + () => { + if (!titleObject || !titleObjectConfiguration) { + return; + } + + const properties = titleObjectConfiguration.getProperties(); + const textProperty = properties.get('text'); + if (!textProperty) { + console.error('Title object does not have a "text" property.'); + return; + } + + const textPropertyValue = textProperty.getValue(); + if (textPropertyValue !== project.getName()) { + project.setName(textPropertyValue); + } + }, + [titleObject, titleObjectConfiguration, project] + ); + + if (!titleObject || !titleObjectConfiguration) { + return ( + + + + + Oops! Looks like this game has no logo set up, you can continue to + the next step. + + + + + ); + } + + return ( + + + + + + ); +}; diff --git a/newIDE/app/src/QuickCustomization/TipCard.js b/newIDE/app/src/QuickCustomization/TipCard.js new file mode 100644 index 000000000000..43d943eee0dc --- /dev/null +++ b/newIDE/app/src/QuickCustomization/TipCard.js @@ -0,0 +1,39 @@ +// @flow +import * as React from 'react'; +import Text from '../UI/Text'; +import { Column, Line } from '../UI/Grid'; +import Paper from '../UI/Paper'; +import Lightbulb from '../UI/CustomSvgIcons/Lightbulb'; +import { ColumnStackLayout } from '../UI/Layout'; + +type Props = {| + title: React.Node, + description: React.Node, +|}; + +const TipCard = ({ title, description }: Props) => { + return ( + + + + + + + + + {title} + + + {description} + + + + + + ); +}; + +export default TipCard; diff --git a/newIDE/app/src/QuickCustomization/index.js b/newIDE/app/src/QuickCustomization/index.js index 9e29a6d3e2f0..12a4d9e5b62f 100644 --- a/newIDE/app/src/QuickCustomization/index.js +++ b/newIDE/app/src/QuickCustomization/index.js @@ -4,52 +4,51 @@ import { QuickObjectReplacer } from './QuickObjectReplacer'; import { QuickBehaviorsTweaker } from './QuickBehaviorsTweaker'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import { QuickPublish } from './QuickPublish'; -import { ColumnStackLayout, LineStackLayout } from '../UI/Layout'; -import Text from '../UI/Text'; -import FlatButton from '../UI/FlatButton'; import { Trans } from '@lingui/macro'; -import PreviewIcon from '../UI/CustomSvgIcons/Preview'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import { mapFor } from '../Utils/MapFor'; import { canSwapAssetOfObject } from '../AssetStore/AssetSwapper'; import { type GameAndBuildsManager } from '../Utils/UseGameAndBuildsManager'; +import { QuickTitleTweaker } from './QuickTitleTweaker'; const gd: libGDevelop = global.gd; -type StepName = 'replace-objects' | 'tweak-behaviors' | 'publish'; +type StepName = 'replace-objects' | 'tweak-behaviors' | 'game-logo' | 'publish'; type Step = {| name: StepName, canPreview: boolean, title: React.Node, - nextLabel: React.Node, - shouldHideNavigationButtons?: boolean, + nextLabel?: React.Node, |}; const steps: Array = [ { name: 'replace-objects', canPreview: true, - title: Personalize your game objects art, + title: Choose your game art, nextLabel: Next: Tweak Gameplay, }, { name: 'tweak-behaviors', canPreview: true, title: Tweak gameplay, - nextLabel: Next: Try & Publish, + nextLabel: Next: Game logo, + }, + { + name: 'game-logo', + canPreview: true, + title: Make your game logo, + nextLabel: Next, }, { name: 'publish', canPreview: false, - title: Publish and try your game, - nextLabel: Finish, - shouldHideNavigationButtons: true, + title: Save your game, }, ]; export type QuickCustomizationState = {| isNavigationDisabled: boolean, - shouldAutomaticallyStartExport: boolean, step: Step, goToNextStep: () => void, goToPreviousStep: () => void, @@ -60,20 +59,15 @@ export type QuickCustomizationState = {| export const useQuickCustomizationState = ({ onClose, }: { - onClose: () => void, + onClose: () => Promise, }): QuickCustomizationState => { const [stepIndex, setStepIndex] = React.useState(0); const [isNavigationDisabled, setIsNavigationDisabled] = React.useState(false); - const [ - shouldAutomaticallyStartExport, - setShouldAutomaticallyStartExport, - ] = React.useState(true); const step = steps[stepIndex]; return { isNavigationDisabled, - shouldAutomaticallyStartExport, step, goToNextStep: React.useCallback( () => { @@ -88,23 +82,19 @@ export const useQuickCustomizationState = ({ ), goToPreviousStep: React.useCallback( () => { - if (step.name === 'publish') { - setShouldAutomaticallyStartExport(false); - } if (stepIndex !== 0) { setStepIndex(stepIndex - 1); } }, - [step, stepIndex] + [stepIndex] ), - canGoToPreviousStep: stepIndex !== 0, + canGoToPreviousStep: stepIndex !== 0 && stepIndex !== steps.length - 1, setIsNavigationDisabled, }; }; export const enumerateObjectFolderOrObjects = ( - objectFolderOrObject: gdObjectFolderOrObject, - depth: number = 0 + objectFolderOrObject: gdObjectFolderOrObject ): Array<{ folderName: string, objects: Array }> => { const orderedFolderNames: Array = ['']; const folderObjects: { [key: string]: Array } = { @@ -125,7 +115,7 @@ export const enumerateObjectFolderOrObjects = ( folderObjects[folderName] || []); orderedFolderNames.push(folderName); - enumerateObjectFolderOrObjects(child, depth + 1).forEach( + enumerateObjectFolderOrObjects(child).forEach( ({ folderName, objects }) => { currentFolderObjects.push.apply(currentFolderObjects, objects); } @@ -154,8 +144,7 @@ type Props = {| onlineWebExporter: Exporter, onSaveProject: () => Promise, isSavingProject: boolean, - - onClose: () => void, + onClose: () => Promise, onContinueQuickCustomization: () => void, onTryAnotherGame: () => void, |}; @@ -169,43 +158,12 @@ export const renderQuickCustomization = ({ onlineWebExporter, onSaveProject, isSavingProject, - onClose, onContinueQuickCustomization, onTryAnotherGame, }: Props) => { return { title: quickCustomizationState.step.title, - titleRightContent: quickCustomizationState.step.canPreview ? ( - - - Preview your game - - Preview} - onClick={onLaunchPreview} - leftIcon={} - /> - - ) : null, - titleTopContent: quickCustomizationState.step.canPreview ? ( - - - - Preview your game - - Preview} - onClick={onLaunchPreview} - leftIcon={} - /> - - - ) : null, content: ( <> {quickCustomizationState.step.name === 'replace-objects' ? ( @@ -218,6 +176,11 @@ export const renderQuickCustomization = ({ project={project} resourceManagementProps={resourceManagementProps} /> + ) : quickCustomizationState.step.name === 'game-logo' ? ( + ) : quickCustomizationState.step.name === 'publish' ? ( ), + showPreview: quickCustomizationState.step.canPreview, }; }; diff --git a/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js b/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js index 8b0ad2e0113f..7abd8c12de98 100644 --- a/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js +++ b/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js @@ -36,12 +36,9 @@ type Props = {| project: gdProject, resourceManagementProps: ResourceManagementProps, resourceKind: ResourceKind, - fallbackResourceKind?: ResourceKind, resourceName: string, defaultNewResourceName?: string, onChange: string => void, - label?: string, - markdownDescription?: ?string, id?: string, |}; @@ -52,9 +49,6 @@ export const CompactResourceSelectorWithThumbnail = ({ resourceName, defaultNewResourceName, onChange, - label, - markdownDescription, - fallbackResourceKind, id, }: Props) => { const resourcesLoader = ResourcesLoader; diff --git a/newIDE/app/src/UI/Accordion.js b/newIDE/app/src/UI/Accordion.js index cbc5ce4dde70..51d88236a503 100644 --- a/newIDE/app/src/UI/Accordion.js +++ b/newIDE/app/src/UI/Accordion.js @@ -135,7 +135,7 @@ type AccordionProps = {| */ export const Accordion = React.forwardRef( (props, ref) => { - const { costlyBody, ...otherProps } = props; + const { costlyBody, noMargin, ...otherProps } = props; const gdevelopTheme = React.useContext(GDevelopThemeContext); return ( @@ -147,12 +147,11 @@ export const Accordion = React.forwardRef( style={{ ...{ border: - !props.noMargin && - `1px solid ${gdevelopTheme.toolbar.separatorColor}`, + !noMargin && `1px solid ${gdevelopTheme.toolbar.separatorColor}`, backgroundColor: gdevelopTheme.paper.backgroundColor.medium, marginLeft: 0, }, - ...(props.noMargin && { + ...(noMargin && { border: `0px`, padding: `0px`, margin: `0px`, diff --git a/newIDE/app/src/UI/CompactToggleField/index.js b/newIDE/app/src/UI/CompactToggleField/index.js index 8860dbd1fd29..b320291758c7 100644 --- a/newIDE/app/src/UI/CompactToggleField/index.js +++ b/newIDE/app/src/UI/CompactToggleField/index.js @@ -1,24 +1,9 @@ // @flow import * as React from 'react'; -import Tooltip from '@material-ui/core/Tooltip'; -import Text from '../../UI/Text'; -import { MarkdownText } from '../../UI/MarkdownText'; -import { tooltipEnterDelay } from '../../UI/Tooltip'; import classes from './CompactToggleField.module.css'; import classNames from 'classnames'; -const styles = { - label: { - overflow: 'hidden', - textOverflow: 'ellipsis', - lineHeight: '17px', - maxHeight: 34, // 2 * lineHeight to limit to 2 lines. - opacity: 0.7, - }, -}; type Props = {| - label: string, - markdownDescription?: ?string, id?: string, checked: boolean, onCheck: (newValue: boolean) => void, @@ -27,40 +12,43 @@ type Props = {| |}; export const CompactToggleField = (props: Props) => { - const title = !props.markdownDescription - ? props.label - : [props.label, ' - ', ]; return (