diff --git a/newIDE/app/src/CompactPropertiesEditor/index.js b/newIDE/app/src/CompactPropertiesEditor/index.js index cc9e3c9d1035..93703c9d742f 100644 --- a/newIDE/app/src/CompactPropertiesEditor/index.js +++ b/newIDE/app/src/CompactPropertiesEditor/index.js @@ -47,7 +47,7 @@ export type ValueFieldCommonProperties = {| getExtraDescription?: Instance => string, hasImpactOnAllOtherFields?: boolean, canBeUnlimitedUsingMinus1?: boolean, - disabled?: (instances: Array) => boolean, + disabled?: (instances: Array) => boolean, onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array, onEditButtonClick?: () => void, getValueFromDisplayedValue?: string => string, @@ -204,6 +204,7 @@ export type Field = removeSpacers?: boolean, title?: ?string, children: Array, + isHidden?: (Array) => boolean, |}; // The schema is the tree of all fields. @@ -217,6 +218,7 @@ type Props = {| mode?: 'column' | 'row', preventWrap?: boolean, removeSpacers?: boolean, + isHidden?: (Array) => boolean, // If set, render the "extra" description content from fields // (see getExtraDescription). @@ -380,6 +382,7 @@ const CompactPropertiesEditor = ({ resourceManagementProps, preventWrap, removeSpacers, + isHidden, }: Props) => { const forceUpdate = useForceUpdate(); @@ -871,7 +874,9 @@ const CompactPropertiesEditor = ({ ); const renderContainer = - mode === 'row' + isHidden && isHidden(instances) + ? (fields: React.Node) => null + : mode === 'row' ? (fields: React.Node) => preventWrap ? ( removeSpacers ? ( @@ -1018,6 +1023,7 @@ const CompactPropertiesEditor = ({ onRefreshAllFields={onRefreshAllFields} preventWrap={field.preventWrap} removeSpacers={field.removeSpacers} + isHidden={field.isHidden} /> ) : (
@@ -1032,6 +1038,7 @@ const CompactPropertiesEditor = ({ onRefreshAllFields={onRefreshAllFields} preventWrap={field.preventWrap} removeSpacers={field.removeSpacers} + isHidden={field.isHidden} />
); diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js index 802ae7eeb7aa..4a50136a4a67 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactInstancePropertiesSchema.js @@ -695,7 +695,7 @@ export const reorderInstanceSchemaForCustomProperties = ( const contentSectionTitle: SectionTitle = { nonFieldType: 'sectionTitle', name: 'Content', - title: 'Content', + title: i18n._(t`Content`), getValue: undefined, }; if (animationFieldIndex === -1) { diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js index 9e095aba98aa..cd621ea84b4c 100644 --- a/newIDE/app/src/InstancesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/index.js @@ -100,7 +100,7 @@ export type InstancesEditorPropsWithoutSizeAndScroll = {| layersContainer: gdLayersContainer, globalObjectsContainer: gdObjectsContainer | null, objectsContainer: gdObjectsContainer, - selectedLayer: string, + chosenLayer: string, initialInstances: gdInitialInstancesContainer, instancesEditorSettings: InstancesEditorSettings, isInstanceOf3DObject: gdInitialInstance => boolean, @@ -1686,7 +1686,7 @@ export default class InstancesEditor extends Component { _instancesAdder.createOrUpdateTemporaryInstancesFromObjectNames( pos, this.props.selectedObjectNames, - this.props.selectedLayer + this.props.chosenLayer ); }} drop={monitor => { diff --git a/newIDE/app/src/LayersList/BackgroundColorRow.js b/newIDE/app/src/LayersList/BackgroundColorRow.js deleted file mode 100644 index 1ca4c94c4239..000000000000 --- a/newIDE/app/src/LayersList/BackgroundColorRow.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow -import { I18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import React from 'react'; -import TextField from '../UI/TextField'; -import ColorPicker from '../UI/ColorField/ColorPicker'; -import { TreeTableCell, TreeTableRow } from '../UI/TreeTable'; -import DragHandle from '../UI/DragHandle'; - -type Props = {| - layout: gdLayout, - onBackgroundColorChanged: () => void, -|}; - -const BackgroundColorRow = ({ layout, onBackgroundColorChanged }: Props) => ( - - {({ i18n }) => ( - - - - - - - - - { - layout.setBackgroundColor(color.rgb.r, color.rgb.g, color.rgb.b); - onBackgroundColorChanged(); - }} - /> - - - )} - -); - -export default BackgroundColorRow; diff --git a/newIDE/app/src/LayersList/BackgroundColorTreeViewItemContent.js b/newIDE/app/src/LayersList/BackgroundColorTreeViewItemContent.js new file mode 100644 index 000000000000..bfcac691c55a --- /dev/null +++ b/newIDE/app/src/LayersList/BackgroundColorTreeViewItemContent.js @@ -0,0 +1,94 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; + +import * as React from 'react'; +import { TreeViewItemContent } from '.'; +import { type HTMLDataset } from '../Utils/HTMLDataset'; +import ColorPicker from '../UI/ColorField/ColorPicker'; + +export const backgroundColorId = 'background-color'; + +export class BackgroundColorTreeViewItemContent implements TreeViewItemContent { + layout: gdLayout; + onBackgroundColorChanged: () => void; + + constructor(layout: gdLayout, onBackgroundColorChanged: () => void) { + this.layout = layout; + this.onBackgroundColorChanged = onBackgroundColorChanged; + } + + getName(i18n: I18nType): string | React.Node { + return i18n._(t`Background color`); + } + + getId(): string { + return backgroundColorId; + } + + getRightButton(i18n: I18nType) { + return []; + } + + getHtmlId(index: number): ?string { + return backgroundColorId; + } + + getDataSet(): ?HTMLDataset { + return null; + } + + getThumbnail(): ?string { + return null; + } + + onClick(): void {} + + buildMenuTemplate(i18n: I18nType, index: number) { + return []; + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return ( + { + this.layout.setBackgroundColor(color.rgb.r, color.rgb.g, color.rgb.b); + this.onBackgroundColorChanged(); + }} + /> + ); + } + + rename(newName: string): void {} + + edit(): void {} + + delete(): void {} + + copy(): void {} + + paste(): void {} + + cut(): void {} + + getIndex(): number { + return 0; + } + + moveAt(destinationIndex: number): void {} + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return false; + } + + getRootId(): string { + return ''; + } +} diff --git a/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/CompactEffectsListEditor.js b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/CompactEffectsListEditor.js new file mode 100644 index 000000000000..a3bc48389dbb --- /dev/null +++ b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/CompactEffectsListEditor.js @@ -0,0 +1,381 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import * as React from 'react'; +import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; +import { type ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; +import { Column, Line, Spacer, marginsSize } from '../../UI/Grid'; +import { Separator } from '../../CompactPropertiesEditor'; +import Text from '../../UI/Text'; +import { Trans, t } from '@lingui/macro'; +import IconButton from '../../UI/IconButton'; +import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal'; +import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource'; +import Paper from '../../UI/Paper'; +import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout'; +import RemoveIcon from '../../UI/CustomSvgIcons/Remove'; +import VisibilityIcon from '../../UI/CustomSvgIcons/Visibility'; +import VisibilityOffIcon from '../../UI/CustomSvgIcons/VisibilityOff'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight'; +import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom'; +import ChevronArrowDownWithRoundedBorder from '../../UI/CustomSvgIcons/ChevronArrowDownWithRoundedBorder'; +import ChevronArrowRightWithRoundedBorder from '../../UI/CustomSvgIcons/ChevronArrowRightWithRoundedBorder'; +import Add from '../../UI/CustomSvgIcons/Add'; +import { CompactEffectPropertiesEditor } from '../../EffectsList/CompactEffectPropertiesEditor'; +import { mapFor } from '../../Utils/MapFor'; +import { + getEnumeratedEffectMetadata, + useManageEffects, +} from '../../EffectsList'; +import CompactSelectField from '../../UI/CompactSelectField'; +import SelectOption from '../../UI/SelectOption'; +import { getHelpLink } from '../../Utils/HelpLink'; +import Window from '../../Utils/Window'; +import { textEllipsisStyle } from '../../UI/TextEllipsis'; +import Link from '../../UI/Link'; +import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow'; + +export const styles = { + icon: { + fontSize: 18, + }, + scrollView: { + paddingTop: marginsSize, + // In theory, should not be needed (the children should be responsible for not + // overflowing the parent). In practice, even when no horizontal scroll is shown + // on Chrome, it might happen on Safari. Prevent any scroll to be 100% sure no + // scrollbar will be shown. + overflowX: 'hidden', + }, + hiddenContent: { display: 'none' }, + subPanelContentContainer: { + display: 'flex', + flexDirection: 'column', + flex: 1, + paddingLeft: marginsSize * 3, + paddingRight: marginsSize, + }, +}; + +const layerEffectsHelpLink = getHelpLink( + '/interface/scene-editor/layer-effects' +); +const objectEffectsHelpLink = getHelpLink('/objects/effects'); + +type TitleBarButton = {| + id: string, + icon: any, + label?: MessageDescriptor, + onClick?: () => void, +|}; + +const CollapsibleSubPanel = ({ + renderContent, + isFolded, + toggleFolded, + title, + titleIcon, + titleBarButtons, +}: {| + renderContent: () => React.Node, + isFolded: boolean, + toggleFolded: () => void, + titleIcon?: ?React.Node, + title: string, + titleBarButtons?: Array, +|}) => ( + + + + + + + {isFolded ? ( + + ) : ( + + )} + + + {titleIcon} + {titleIcon && } + + {title} + + + + {titleBarButtons && + titleBarButtons.map(button => { + const Icon = button.icon; + return ( + + + + ); + })} + + + + {isFolded ? null : ( +
{renderContent()}
+ )} +
+
+
+); + +const TopLevelCollapsibleSection = ({ + title, + isFolded, + toggleFolded, + renderContent, + renderContentAsHiddenWhenFolded, + noContentMargin, + onOpenFullEditor, + onAdd, +}: {| + title: React.Node, + isFolded: boolean, + toggleFolded: () => void, + renderContent: () => React.Node, + renderContentAsHiddenWhenFolded?: boolean, + noContentMargin?: boolean, + onOpenFullEditor: () => void, + onAdd?: (() => void) | null, +|}) => ( + <> + + + + + + {isFolded ? ( + + ) : ( + + )} + + + {title} + + + + + + + {onAdd && ( + + + + )} + + + + + {isFolded ? ( + renderContentAsHiddenWhenFolded ? ( +
{renderContent()}
+ ) : null + ) : ( + renderContent() + )} +
+ +); + +type Props = {| + project: gdProject, + resourceManagementProps: ResourceManagementProps, + projectScopedContainersAccessor: ProjectScopedContainersAccessor, + unsavedChanges?: ?UnsavedChanges, + i18n: I18nType, + + effectsContainer: gdEffectsContainer, + onEffectsUpdated: () => void, + onOpenFullEditor: () => void, + onEffectAdded: () => void, + layerRenderingType: '2d' | '3d', + target: 'object' | 'layer', +|}; + +export const CompactEffectsListEditor = ({ + project, + resourceManagementProps, + projectScopedContainersAccessor, + unsavedChanges, + i18n, + effectsContainer, + onEffectsUpdated, + onOpenFullEditor, + onEffectAdded, + layerRenderingType, + target, +}: Props) => { + const forceUpdate = useForceUpdate(); + const [isEffectsFolded, setEffectsFolded] = React.useState(false); + + // Effects: + const { + allEffectMetadata, + all2DEffectMetadata, + all3DEffectMetadata, + addEffect, + removeEffect, + chooseEffectType, + } = useManageEffects({ + effectsContainer, + project, + onEffectsUpdated: () => { + onEffectsUpdated(); + forceUpdate(); + }, + onEffectAdded, + onUpdate: forceUpdate, + target, + }); + + const filteredEffectMetadata = + layerRenderingType === '3d' ? all3DEffectMetadata : all2DEffectMetadata; + + return ( + Effects + ) : layerRenderingType === '3d' ? ( + 3D effects + ) : ( + 2D effects + ) + } + isFolded={isEffectsFolded} + toggleFolded={() => setEffectsFolded(!isEffectsFolded)} + onOpenFullEditor={onOpenFullEditor} + onAdd={() => addEffect(layerRenderingType === '3d')} + renderContent={() => ( + + {effectsContainer.getEffectsCount() === 0 && ( + + {target === 'object' ? ( + + There are no{' '} + + Window.openExternalURL(objectEffectsHelpLink) + } + > + effects + {' '} + on this object. + + ) : layerRenderingType === '3d' ? ( + + There are no{' '} + Window.openExternalURL(layerEffectsHelpLink)} + > + 3D effects + {' '} + on this layer. + + ) : ( + + There are no{' '} + Window.openExternalURL(layerEffectsHelpLink)} + > + 2D effects + {' '} + on this layer. + + )} + + )} + {mapFor(0, effectsContainer.getEffectsCount(), (index: number) => { + const effect: gdEffect = effectsContainer.getEffectAt(index); + const effectType = effect.getEffectType(); + const effectMetadata = getEnumeratedEffectMetadata( + allEffectMetadata, + effectType + ); + + return !effectMetadata || + (layerRenderingType !== '3d' && + !effectMetadata.isMarkedAsOnlyWorkingFor3D) || + (layerRenderingType !== '2d' && + !effectMetadata.isMarkedAsOnlyWorkingFor2D) ? ( + ( + + chooseEffectType(effect, type)} + > + {filteredEffectMetadata.map(effectMetadata => ( + + ))} + + + + )} + isFolded={effect.isFolded()} + toggleFolded={() => { + effect.setFolded(!effect.isFolded()); + forceUpdate(); + }} + title={effect.getName()} + titleBarButtons={[ + { + id: 'effect-visibility', + icon: effect.isEnabled() + ? VisibilityIcon + : VisibilityOffIcon, + label: effect.isEnabled() ? t`Hide effect` : t`Show effect`, + onClick: () => { + effect.setEnabled(!effect.isEnabled()); + onEffectsUpdated(); + forceUpdate(); + }, + }, + { + id: 'remove-effect', + icon: RemoveIcon, + label: t`Remove effect`, + onClick: () => { + removeEffect(effect); + onEffectsUpdated(); + }, + }, + ]} + /> + ) : null; + })} + + )} + /> + ); +}; diff --git a/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/CompactLayerPropertiesSchema.js b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/CompactLayerPropertiesSchema.js new file mode 100644 index 000000000000..37541d05c6cc --- /dev/null +++ b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/CompactLayerPropertiesSchema.js @@ -0,0 +1,226 @@ +// @flow + +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; +import { type Schema } from '../../CompactPropertiesEditor'; +import { + rgbColorToRGBString, + rgbStringAndAlphaToRGBColor, +} from '../../Utils/ColorTransformer'; + +const defaultCameraBehaviorChoices = [ + { + value: 'do-nothing', + label: t`Keep centered (best for game content)`, + }, + { + value: 'top-left-anchored-if-never-moved', + label: t`Keep top-left corner fixed (best for content that can extend)`, + }, +]; +const getDefaultCameraBehaviorField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Default camera behavior', + getLabel: () => i18n._(t`Default camera behavior`), + valueType: 'string', + getChoices: () => defaultCameraBehaviorChoices, + getValue: (layer: gdLayer) => layer.getDefaultCameraBehavior(), + setValue: (layer: gdLayer, newValue: string) => + layer.setDefaultCameraBehavior(newValue), +}); + +const renderingTypeChoices = [ + { + value: '', + label: t`Display both 2D and 3D objects (default)`, + }, + { + value: '2d', + label: t`Force display only 2D objects`, + }, + { + value: '3d', + label: t`Force display only 3D objects`, + }, + { + value: '2d+3d', + label: t`Force display both 2D and 3D objects`, + }, +]; +const getRenderingTypeField = ({ + i18n, + forceUpdate, +}: {| + i18n: I18nType, + forceUpdate: () => void, +|}) => ({ + name: 'Rendering type', + getLabel: () => i18n._(t`Rendering type`), + valueType: 'string', + getChoices: () => renderingTypeChoices, + getValue: (layer: gdLayer) => layer.getRenderingType(), + setValue: (layer: gdLayer, newValue: string) => { + layer.setRenderingType(newValue); + forceUpdate(); + }, +}); + +const cameraTypeChoices = [ + { + value: 'perspective', + label: t`Perspective camera`, + }, + { + value: 'orthographic', + label: t`Orthographic camera`, + }, +]; +const getCameraTypeField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Camera type', + getLabel: () => i18n._(t`Camera type`), + valueType: 'string', + getChoices: () => cameraTypeChoices, + getValue: (layer: gdLayer) => layer.getCameraType(), + // TODO checkNearPlaneDistanceError + setValue: (layer: gdLayer, newValue: string) => layer.setCameraType(newValue), +}); + +const getCamera3DFieldOfViewField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Field of view', + getLabel: () => i18n._(t`Field of view (in degrees)`), + valueType: 'number', + getValue: (layer: gdLayer) => layer.getCamera3DFieldOfView(), + // TODO onChangeCamera3DFieldOfView + setValue: (layer: gdLayer, newValue: number) => + layer.setCamera3DFieldOfView(newValue), + disabled: (layers: Array) => + layers[0] && layers[0].getCameraType() !== 'perspective', +}); + +const getNearPlaneDistanceField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Near plane distance', + getLabel: () => i18n._(t`Near plane distance`), + valueType: 'number', + getValue: (layer: gdLayer) => layer.getCamera3DNearPlaneDistance(), + // TODO onChangeCamera3DNearPlaneDistance + setValue: (layer: gdLayer, newValue: number) => + layer.setCamera3DNearPlaneDistance(newValue), +}); + +const getFarPlaneDistanceField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Far plane distance', + getLabel: () => i18n._(t`Far plane distance`), + valueType: 'number', + getValue: (layer: gdLayer) => layer.getCamera3DFarPlaneDistance(), + // TODO onChangeCamera3DNearPlaneDistance + setValue: (layer: gdLayer, newValue: number) => + layer.setCamera3DFarPlaneDistance(newValue), +}); + +const getCamera2DPlaneMaxDrawingDistanceField = ({ + i18n, +}: {| + i18n: I18nType, +|}) => ({ + name: 'Maximum 2D drawing distance', + getLabel: () => i18n._(t`Maximum 2D drawing distance`), + valueType: 'number', + getValue: (layer: gdLayer) => layer.getCamera2DPlaneMaxDrawingDistance(), + // TODO onChangeCamera2DPlaneMaxDrawingDistance + setValue: (layer: gdLayer, newValue: number) => + layer.setCamera2DPlaneMaxDrawingDistance(newValue), +}); + +const getFollowingBaseLayerCameraField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Automatically follow the base layer', + getLabel: () => i18n._(t`Automatically follow the base layer.`), + valueType: 'boolean', + getValue: (layer: gdLayer) => layer.isFollowingBaseLayerCamera(), + setValue: (layer: gdLayer, newValue: boolean) => + layer.setFollowBaseLayerCamera(newValue), +}); + +const getAmbientLightColorField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Ambient light color', + getLabel: () => i18n._(t`Ambient light color`), + valueType: 'color', + getValue: (layer: gdLayer) => + rgbColorToRGBString({ + r: layer.getAmbientLightColorRed(), + g: layer.getAmbientLightColorGreen(), + b: layer.getAmbientLightColorBlue(), + }), + setValue: (layer: gdLayer, newColor: string) => { + const currentRgbColor = { + r: layer.getAmbientLightColorRed(), + g: layer.getAmbientLightColorGreen(), + b: layer.getAmbientLightColorBlue(), + }; + const newRgbColor = rgbStringAndAlphaToRGBColor(newColor); + if ( + newRgbColor && + (newRgbColor.r !== currentRgbColor.r || + newRgbColor.g !== currentRgbColor.g || + newRgbColor.b !== currentRgbColor.b) + ) { + layer.setAmbientLightColor(newRgbColor.r, newRgbColor.g, newRgbColor.b); + } + }, +}); + +export const makeSchema = ({ + i18n, + forceUpdate, + onEditLayer, + layersContainer, +}: {| + i18n: I18nType, + forceUpdate: () => void, + onEditLayer: (layer: gdLayer) => void, + layersContainer: gdLayersContainer, +|}): Schema => { + return [ + getDefaultCameraBehaviorField({ i18n }), + { + name: 'Not a lighting layer', + type: 'column', + isHidden: layers => layers[0] && layers[0].isLightingLayer(), + children: [ + getRenderingTypeField({ i18n, forceUpdate }), + { + name: 'Optional 3D settings', + type: 'column', + isHidden: layers => + layers[0] && layers[0].getRenderingType() === '2d', + children: [ + { + name: '3D settings', + title: i18n._(t`3D settings`), + nonFieldType: 'sectionTitle', + getValue: undefined, + }, + getCameraTypeField({ i18n }), + getCamera3DFieldOfViewField({ i18n }), + getNearPlaneDistanceField({ i18n }), + getFarPlaneDistanceField({ i18n }), + getCamera2DPlaneMaxDrawingDistanceField({ i18n }), + ], + }, + ], + }, + { + name: 'Optional lighting layer settings', + type: 'column', + isHidden: layers => layers[0] && !layers[0].isLightingLayer(), + children: [ + { + name: 'Lighting settings', + title: i18n._(t`Lighting settings`), + nonFieldType: 'sectionTitle', + getValue: undefined, + }, + getFollowingBaseLayerCameraField({ i18n }), + getAmbientLightColorField({ i18n }), + ], + }, + ].filter(Boolean); +}; diff --git a/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js new file mode 100644 index 000000000000..296543f3d130 --- /dev/null +++ b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js @@ -0,0 +1,272 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import * as React from 'react'; +import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; +import { type ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; +import ErrorBoundary from '../../UI/ErrorBoundary'; +import ScrollView from '../../UI/ScrollView'; +import { Column, Line, marginsSize } from '../../UI/Grid'; +import CompactPropertiesEditor, { + Separator, +} from '../../CompactPropertiesEditor'; +import Text from '../../UI/Text'; +import { Trans, t } from '@lingui/macro'; +import IconButton from '../../UI/IconButton'; +import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal'; +import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource'; +import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import ChevronArrowDownWithRoundedBorder from '../../UI/CustomSvgIcons/ChevronArrowDownWithRoundedBorder'; +import ChevronArrowRightWithRoundedBorder from '../../UI/CustomSvgIcons/ChevronArrowRightWithRoundedBorder'; +import Add from '../../UI/CustomSvgIcons/Add'; +import LayersIcon from '../../UI/CustomSvgIcons/Layers'; +import Help from '../../UI/CustomSvgIcons/Help'; +import { getHelpLink } from '../../Utils/HelpLink'; +import Window from '../../Utils/Window'; +import CompactTextField from '../../UI/CompactTextField'; +import { textEllipsisStyle } from '../../UI/TextEllipsis'; +import { makeSchema } from './CompactLayerPropertiesSchema'; +import { type Schema } from '../../CompactPropertiesEditor'; +import { CompactEffectsListEditor } from './CompactEffectsListEditor'; + +export const styles = { + icon: { + fontSize: 18, + }, + scrollView: { + paddingTop: marginsSize, + // In theory, should not be needed (the children should be responsible for not + // overflowing the parent). In practice, even when no horizontal scroll is shown + // on Chrome, it might happen on Safari. Prevent any scroll to be 100% sure no + // scrollbar will be shown. + overflowX: 'hidden', + }, + hiddenContent: { display: 'none' }, + subPanelContentContainer: { + display: 'flex', + flexDirection: 'column', + flex: 1, + paddingLeft: marginsSize * 3, + paddingRight: marginsSize, + }, +}; + +const noRefreshOfAllFields = () => { + console.warn( + "An instance tried to refresh all fields, but the editor doesn't support it." + ); +}; + +const effectsHelpLink = getHelpLink( + '/interface/scene-editor/layers-and-cameras' +); + +const TopLevelCollapsibleSection = ({ + title, + isFolded, + toggleFolded, + renderContent, + renderContentAsHiddenWhenFolded, + noContentMargin, + onOpenFullEditor, + onAdd, +}: {| + title: React.Node, + isFolded: boolean, + toggleFolded: () => void, + renderContent: () => React.Node, + renderContentAsHiddenWhenFolded?: boolean, + noContentMargin?: boolean, + onOpenFullEditor: () => void, + onAdd?: (() => void) | null, +|}) => ( + <> + + + + + + {isFolded ? ( + + ) : ( + + )} + + + {title} + + + + + + + {onAdd && ( + + + + )} + + + + + {isFolded ? ( + renderContentAsHiddenWhenFolded ? ( +
{renderContent()}
+ ) : null + ) : ( + renderContent() + )} +
+ +); + +type Props = {| + project: gdProject, + resourceManagementProps: ResourceManagementProps, + layersContainer: gdLayersContainer, + projectScopedContainersAccessor: ProjectScopedContainersAccessor, + unsavedChanges?: ?UnsavedChanges, + i18n: I18nType, + + layer: gdLayer, + onEditLayer: (layer: gdLayer) => void, + onEditLayerEffects: (layer: gdLayer) => void, + onLayersModified: (layers: Array) => void, + onEffectAdded: () => void, +|}; + +export const CompactLayerPropertiesEditor = ({ + project, + resourceManagementProps, + layersContainer, + projectScopedContainersAccessor, + unsavedChanges, + i18n, + layer, + onEditLayer, + onEditLayerEffects, + onLayersModified, + onEffectAdded, +}: Props) => { + const forceUpdate = useForceUpdate(); + const [isPropertiesFoldedOrDefault, setIsPropertiesFolded] = React.useState< + boolean | null + >(null); + const isPropertiesFolded = + isPropertiesFoldedOrDefault === null + ? !layer.isLightingLayer() + : !!isPropertiesFoldedOrDefault; + + // Properties: + const { object, instanceSchema } = React.useMemo<{| + object?: gdObject, + instanceSchema?: Schema, + |}>( + () => { + const instanceSchema = makeSchema({ + i18n, + onEditLayer, + layersContainer, + forceUpdate, + }); + return { + object, + instanceSchema, + }; + }, + [i18n, onEditLayer, layersContainer, forceUpdate] + ); + + const openFullEditor = React.useCallback(() => onEditLayer(layer), [ + layer, + onEditLayer, + ]); + + return ( + Layer properties} + scope="scene-editor-layer-properties" + > + + + + + + + + Layer + + { + Window.openExternalURL(effectsHelpLink); + }} + > + + + + + {}} + disabled + /> + + {instanceSchema && ( + Properties} + isFolded={isPropertiesFolded} + toggleFolded={() => setIsPropertiesFolded(!isPropertiesFolded)} + onOpenFullEditor={openFullEditor} + renderContent={() => ( + + + + )} + /> + )} + {layer.getRenderingType() !== '3d' && ( + onLayersModified([layer])} + onOpenFullEditor={() => onEditLayerEffects(layer)} + onEffectAdded={onEffectAdded} + /> + )} + {layer.getRenderingType() !== '2d' && !layer.isLightingLayer() && ( + onLayersModified([layer])} + onOpenFullEditor={() => onEditLayerEffects(layer)} + onEffectAdded={onEffectAdded} + /> + )} + + + + ); +}; diff --git a/newIDE/app/src/LayersList/LayerRow.js b/newIDE/app/src/LayersList/LayerRow.js deleted file mode 100644 index 894c33c48383..000000000000 --- a/newIDE/app/src/LayersList/LayerRow.js +++ /dev/null @@ -1,274 +0,0 @@ -// @flow -import * as React from 'react'; -import { I18n } from '@lingui/react'; -import { type I18n as I18nType } from '@lingui/core'; -import Radio from '@material-ui/core/Radio'; -import Tooltip from '@material-ui/core/Tooltip'; -import { t, Trans } from '@lingui/macro'; -import { TreeTableRow, TreeTableCell } from '../UI/TreeTable'; -import InlineCheckbox from '../UI/InlineCheckbox'; -import IconButton from '../UI/IconButton'; -import SemiControlledTextField from '../UI/SemiControlledTextField'; -import DragHandle from '../UI/DragHandle'; -import ElementWithMenu from '../UI/Menu/ElementWithMenu'; -import Badge from '../UI/Badge'; -import { makeDragSourceAndDropTarget } from '../UI/DragAndDrop/DragSourceAndDropTarget'; -import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; - -import ThreeDotsMenu from '../UI/CustomSvgIcons/ThreeDotsMenu'; -import VisibilityIcon from '../UI/CustomSvgIcons/Visibility'; -import LockIcon from '../UI/CustomSvgIcons/Lock'; -import LockOpenIcon from '../UI/CustomSvgIcons/LockOpen'; -import VisibilityOffIcon from '../UI/CustomSvgIcons/VisibilityOff'; -import TrashIcon from '../UI/CustomSvgIcons/Trash'; -import EditIcon from '../UI/CustomSvgIcons/Edit'; -import LightbulbIcon from '../UI/CustomSvgIcons/Lightbulb'; -import LightModeIcon from '../UI/CustomSvgIcons/LightMode'; -import Object2dIcon from '../UI/CustomSvgIcons/Object2d'; -import Object3dIcon from '../UI/CustomSvgIcons/Object3d'; -import Layer2dAnd3dIcon from '../UI/CustomSvgIcons/Layer2dAnd3d'; - -const DragSourceAndDropTarget = makeDragSourceAndDropTarget('layers-list'); - -export const styles = { - dropIndicator: { - outline: '1px solid white', - }, -}; - -type Props = {| - id: string, - layer: gdLayer, - isSelected: boolean, - onSelect: string => void, - nameError: React.Node, - onBlur: string => void, - onRemove: () => void, - onBeginDrag: () => void, - onDrop: () => void, - isVisible: boolean, - onChangeVisibility: boolean => void, - isLocked: boolean, - onChangeLockState: boolean => void, - effectsCount: number, - onEditLayerEffects: () => void, - onEdit: () => void, - width: number, -|}; - -const LayerRow = ({ - id, - layer, - isSelected, - onSelect, - nameError, - onBlur, - onRemove, - isVisible, - isLocked, - onChangeLockState, - effectsCount, - onEditLayerEffects, - onChangeVisibility, - onBeginDrag, - onDrop, - width, - onEdit, -}: Props) => { - const gdevelopTheme = React.useContext(GDevelopThemeContext); - - const layerName = layer.getName(); - const isLightingLayer = layer.isLightingLayer(); - const renderingType = layer.getRenderingType(); - - const editPropertiesIcon = isLightingLayer ? ( - - ) : renderingType === '2d' ? ( - - ) : renderingType === '3d' ? ( - - ) : renderingType === '2d+3d' ? ( - - ) : ( - - ); - - const isBaseLayer = !layerName; - - return ( - - {({ i18n }) => ( - { - onBeginDrag(); - return {}; - }} - canDrag={() => true} - canDrop={() => true} - drop={onDrop} - > - {({ connectDragSource, connectDropTarget, isOver, canDrop }) => - connectDropTarget( -
- {isOver && ( -
- )} - - - {connectDragSource( - - - - )} - - - - Layer where instances are added by default - - } - > - - - - - - - - {width < 350 ? ( - - - - } - buildMenuTemplate={(i18n: I18nType) => [ - { - label: isLightingLayer - ? i18n._(t`Edit lighting properties`) - : i18n._(t`Edit properties`), - click: onEdit, - }, - { - label: i18n._(t`Edit effects (${effectsCount})`), - click: onEditLayerEffects, - }, - { - type: 'checkbox', - label: i18n._(t`Visible`), - checked: isVisible, - click: () => onChangeVisibility(!isVisible), - }, - { - type: 'checkbox', - label: i18n._(t`Locked`), - enabled: isVisible, - checked: isLocked || !isVisible, - click: () => onChangeLockState(!isLocked), - }, - { type: 'separator' }, - { - label: i18n._(t`Delete`), - enabled: !isBaseLayer, - click: onRemove, - }, - ]} - /> - ) : ( - - } - uncheckedIcon={} - onCheck={(e, value) => onChangeVisibility(value)} - tooltipOrHelperText={ - isVisible ? ( - Hide layer - ) : ( - Show layer - ) - } - /> - } - uncheckedIcon={} - onCheck={(e, value) => onChangeLockState(value)} - tooltipOrHelperText={ - isLocked ? ( - Unlock layer - ) : ( - Lock layer - ) - } - /> - - - - - - - {editPropertiesIcon} - - - - - - )} - - -
- ) - } - - )} - - ); -}; - -export default LayerRow; diff --git a/newIDE/app/src/LayersList/LayerTreeViewItemContent.js b/newIDE/app/src/LayersList/LayerTreeViewItemContent.js new file mode 100644 index 000000000000..6793b6f17f68 --- /dev/null +++ b/newIDE/app/src/LayersList/LayerTreeViewItemContent.js @@ -0,0 +1,245 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import { t, Trans } from '@lingui/macro'; + +import * as React from 'react'; +import { TreeViewItemContent, type TreeItemProps, layersRootFolderId } from '.'; +import Tooltip from '@material-ui/core/Tooltip'; +import { type HTMLDataset } from '../Utils/HTMLDataset'; +import VisibilityIcon from '../UI/CustomSvgIcons/Visibility'; +import VisibilityOffIcon from '../UI/CustomSvgIcons/VisibilityOff'; +import LockIcon from '../UI/CustomSvgIcons/Lock'; +import LockOpenIcon from '../UI/CustomSvgIcons/LockOpen'; +import Radio from '@material-ui/core/Radio'; + +const styles = { + tooltip: { marginRight: 5, verticalAlign: 'bottom' }, +}; + +export type LayerTreeViewItemProps = {| + ...TreeItemProps, + layersContainer: gdLayersContainer, + chosenLayer: string, + onChooseLayer: (layerName: string) => void, + onSelectLayer: (layer: gdLayer | null) => void, + onEditLayer: (layer: ?gdLayer) => void, + onDeleteLayer: (layer: gdLayer) => void, + onLayersModified: () => void, + onRenameLayer: (oldName: string, newName: string) => void, + triggerOnLayersModified: () => void, +|}; + +export const getLayerTreeViewItemId = (layer: gdLayer): string => { + // Pointers are used because they stay the same even when the names are + // changed. + return `layer-${layer.ptr}`; +}; + +export class LayerTreeViewItemContent implements TreeViewItemContent { + layer: gdLayer; + props: LayerTreeViewItemProps; + + constructor(layer: gdLayer, props: LayerTreeViewItemProps) { + this.layer = layer; + this.props = props; + } + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return itemContent.getId() === layersRootFolderId; + } + + getRootId(): string { + // This is not actually a parent, but it's useful to check where layers + // can be dropped. + return layersRootFolderId; + } + + getName(i18n: I18nType): string | React.Node { + return this._isBaseLayer() ? i18n._(t`Base layer`) : this.layer.getName(); + } + + _isBaseLayer() { + return !this.layer.getName(); + } + + getId(): string { + return getLayerTreeViewItemId(this.layer); + } + + getHtmlId(nodeIndex: number): ?string { + // This index is not reversed with `_getRevertedIndex` + return `layer-${this.props.layersContainer.getLayerPosition( + this.layer.getName() + )}`; + } + + getDataSet(): ?HTMLDataset { + return { + scene: this.layer.getName(), + }; + } + + getThumbnail(): ?string { + return null; + } + + onClick(): void { + this.props.onSelectLayer(this.layer); + } + + rename(newName: string): void { + if (!newName) { + return; + } + const oldName = this.layer.getName(); + if (oldName === newName) { + return; + } + this.props.onRenameLayer(oldName, newName); + } + + edit(): void { + this.props.onEditLayer(this.layer); + this.props.onSelectLayer(null); + } + + _isVisible(): boolean { + return this.layer.getVisibility(); + } + + _isLocked(): boolean { + return this.layer.isLocked(); + } + + _setVisibility(visible: boolean): void { + this.layer.setVisibility(visible); + this.props.triggerOnLayersModified(); + } + + _setLocked(isLocked: boolean): void { + this.layer.setLocked(isLocked); + this.props.triggerOnLayersModified(); + } + + getRightButton(i18n: I18nType) { + return [ + { + icon: this._isVisible() ? : , + label: i18n._(t`Visible`), + click: () => this._setVisibility(!this._isVisible()), + id: 'layer-visibility', + }, + { + icon: + this._isLocked() || !this._isVisible() ? ( + + ) : ( + + ), + label: i18n._(t`Locked`), + enabled: this._isVisible(), + click: () => this._setLocked(!this._isLocked()), + id: 'layer-lock', + }, + ]; + } + + buildMenuTemplate(i18n: I18nType, index: number) { + return [ + { + label: i18n._(t`Rename`), + click: () => this.props.editName(this.getId()), + accelerator: 'F2', + enabled: !this._isBaseLayer(), + }, + { + label: i18n._(t`Delete`), + click: () => this.delete(), + accelerator: 'Backspace', + enabled: !this._isBaseLayer(), + }, + { + type: 'separator', + }, + { + label: i18n._(t`Open layer editor`), + click: () => { + this.props.onEditLayer(this.layer); + this.props.onSelectLayer(null); + }, + }, + { + type: 'separator', + }, + { + type: 'checkbox', + label: i18n._(t`Visible`), + checked: this._isVisible(), + click: () => this._setVisibility(!this._isVisible()), + }, + { + type: 'checkbox', + label: i18n._(t`Locked`), + enabled: this._isVisible(), + checked: this._isLocked() || !this._isVisible(), + click: () => this._setLocked(!this._isLocked()), + }, + ]; + } + + _isChosenLayer(): boolean { + return this.layer.getName() === this.props.chosenLayer; + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return ( + Layer where instances are added by default} + > + this.props.onChooseLayer(this.layer.getName())} + size="small" + id={`layer-selected-${ + this._isChosenLayer() ? 'checked' : 'unchecked' + }`} + /> + + ); + } + + delete(): void { + this.props.onDeleteLayer(this.layer); + } + + getIndex(): number { + return this._getRevertedIndex( + this.props.layersContainer.getLayerPosition(this.layer.getName()) + ); + } + + _getRevertedIndex(index: number): number { + return this.props.layersContainer.getLayersCount() - 1 - index; + } + + moveAt(destinationIndex: number): void { + const originIndex = this.getIndex(); + if (destinationIndex !== originIndex) { + this.props.layersContainer.moveLayer( + this._getRevertedIndex(originIndex), + // When moving the item down, it must not be counted. + this._getRevertedIndex( + destinationIndex + (destinationIndex <= originIndex ? 0 : -1) + ) + ); + this._onProjectItemModified(); + } + } + + _onProjectItemModified() { + if (this.props.unsavedChanges) + this.props.unsavedChanges.triggerUnsavedChanges(); + this.props.forceUpdate(); + } +} diff --git a/newIDE/app/src/LayersList/index.js b/newIDE/app/src/LayersList/index.js index c256fc9d8811..1ca358ab529d 100644 --- a/newIDE/app/src/LayersList/index.js +++ b/newIDE/app/src/LayersList/index.js @@ -1,235 +1,258 @@ // @flow -import { t, Trans } from '@lingui/macro'; +import { Trans } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; + import * as React from 'react'; import newNameGenerator from '../Utils/NewNameGenerator'; -import { mapReverseFor } from '../Utils/MapFor'; -import LayerRow, { styles } from './LayerRow'; -import BackgroundColorRow from './BackgroundColorRow'; -import { Column } from '../UI/Grid'; -import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; -import ScrollView from '../UI/ScrollView'; -import { FullSizeMeasurer } from '../UI/FullSizeMeasurer'; -import Background from '../UI/Background'; +import UnsavedChangesContext, { + type UnsavedChanges, +} from '../MainFrame/UnsavedChangesContext'; import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; -import RaisedButtonWithSplitMenu from '../UI/RaisedButtonWithSplitMenu'; +import ErrorBoundary from '../UI/ErrorBoundary'; import useForceUpdate from '../Utils/UseForceUpdate'; -import { makeDropTarget } from '../UI/DragAndDrop/DropTarget'; -import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; + +import { AutoSizer } from 'react-virtualized'; +import Background from '../UI/Background'; +import TreeView, { + type TreeViewInterface, + type MenuButton, +} from '../UI/TreeView'; +import PreferencesContext, { + type Preferences, +} from '../MainFrame/Preferences/PreferencesContext'; import Add from '../UI/CustomSvgIcons/Add'; +import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext'; +import KeyboardShortcuts from '../UI/KeyboardShortcuts'; +import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; +import { + LayerTreeViewItemContent, + getLayerTreeViewItemId, + type LayerTreeViewItemProps, +} from './LayerTreeViewItemContent'; +import { + BackgroundColorTreeViewItemContent, + backgroundColorId, +} from './BackgroundColorTreeViewItemContent'; +import { type MenuItemTemplate } from '../UI/Menu/Menu.flow'; +import useAlertDialog from '../UI/Alert/useAlertDialog'; +import { type ShowConfirmDeleteDialogOptions } from '../UI/Alert/AlertContext'; +import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; +import { type GDevelopTheme } from '../UI/Theme'; +import { type HTMLDataset } from '../Utils/HTMLDataset'; +import LightbulbIconOn from '../UI/CustomSvgIcons/LightbulbOn'; +import LightbulbIconOff from '../UI/CustomSvgIcons/LightbulbOff'; +import { mapReverseFor } from '../Utils/MapFor'; import { addDefaultLightToLayer } from '../ProjectCreation/CreateProject'; -import { getEffects2DCount, getEffects3DCount } from '../EffectsList'; -import ErrorBoundary from '../UI/ErrorBoundary'; -import IconButton from '../UI/IconButton'; -import LightbulbIcon from '../UI/CustomSvgIcons/Lightbulb'; -import { LineStackLayout } from '../UI/Layout'; const gd: libGDevelop = global.gd; -const DropTarget = makeDropTarget('layers-list'); +export const layersRootFolderId = 'layers'; -type LayersListBodyProps = {| - project: gdProject, - layout: gdLayout | null, - eventsFunctionsExtension: gdEventsFunctionsExtension | null, - eventsBasedObject: gdEventsBasedObject | null, - layersContainer: gdLayersContainer, - selectedLayer: string, - onSelectLayer: string => void, +const styles = { + listContainer: { + flex: 1, + display: 'flex', + flexDirection: 'column', + padding: '0 8px 8px 8px', + }, + autoSizerContainer: { flex: 1 }, + autoSizer: { width: '100%' }, +}; + +const extensionItemReactDndType = 'GD_EXTENSION_ITEM'; + +const hasLightingLayer = (layersContainer: gdLayersContainer) => { + const layersCount = layersContainer.getLayersCount(); + return ( + mapReverseFor(0, layersCount, i => + layersContainer.getLayerAt(i).isLightingLayer() + ).filter(Boolean).length > 0 + ); +}; + +export interface TreeViewItemContent { + getName(i18n: I18nType): string | React.Node; + getId(): string; + getHtmlId(index: number): ?string; + getDataSet(): ?HTMLDataset; + getThumbnail(): ?string; + onClick(): void; + buildMenuTemplate(i18n: I18nType, index: number): Array; + getRightButton(i18n: I18nType): Array; + renderRightComponent(i18n: I18nType): ?React.Node; + rename(newName: string): void; + edit(): void; + delete(): void; + getIndex(): number; + moveAt(destinationIndex: number): void; + isDescendantOf(itemContent: TreeViewItemContent): boolean; + getRootId(): string; +} + +interface TreeViewItem { + isRoot?: boolean; + isPlaceholder?: boolean; + +content: TreeViewItemContent; + getChildren(i18n: I18nType): ?Array; +} + +export type TreeItemProps = {| + forceUpdate: () => void, + forceUpdateList: () => void, unsavedChanges?: ?UnsavedChanges, - onRemoveLayer: (layerName: string, cb: (done: boolean) => void) => void, - onLayerRenamed: () => void, - onEditLayerEffects: (layer: ?gdLayer) => void, - onEdit: (layer: ?gdLayer) => void, - onLayersModified: () => void, - onBackgroundColorChanged: () => void, - width: number, + preferences: Preferences, + gdevelopTheme: GDevelopTheme, + project: gdProject, + editName: (itemId: string) => void, + scrollToItem: (itemId: string) => void, + showDeleteConfirmation: ( + options: ShowConfirmDeleteDialogOptions + ) => Promise, |}; -const getEffectsCount = (platform: gdPlatform, layer: gdLayer) => { - const effectsContainer = layer.getEffects(); - return layer.getRenderingType() === '2d' - ? getEffects2DCount(platform, effectsContainer) - : layer.getRenderingType() === '3d' - ? getEffects3DCount(platform, effectsContainer) - : effectsContainer.getEffectsCount(); -}; +class LeafTreeViewItem implements TreeViewItem { + content: TreeViewItemContent; -const LayersListBody = ({ - project, - layout, - eventsFunctionsExtension, - eventsBasedObject, - layersContainer, - onEditLayerEffects, - onEdit, - width, - onLayerRenamed, - onRemoveLayer, - unsavedChanges, - selectedLayer, - onSelectLayer, - onLayersModified, - onBackgroundColorChanged, -}: LayersListBodyProps) => { - const forceUpdate = useForceUpdate(); - const gdevelopTheme = React.useContext(GDevelopThemeContext); - const [nameErrors, setNameErrors] = React.useState<{ - [key: string]: React.Node, - }>({}); - const draggedLayerIndexRef = React.useRef(null); - - const triggerOnLayersModified = React.useCallback( - () => { - onLayersModified(); - if (unsavedChanges) unsavedChanges.triggerUnsavedChanges(); - forceUpdate(); - }, - [forceUpdate, onLayersModified, unsavedChanges] - ); + constructor(content: TreeViewItemContent) { + this.content = content; + } - const triggerOnBackgroundColorChanged = React.useCallback( - () => { - onBackgroundColorChanged(); - if (unsavedChanges) unsavedChanges.triggerUnsavedChanges(); - forceUpdate(); - }, - [forceUpdate, onBackgroundColorChanged, unsavedChanges] - ); + getChildren(i18n: I18nType): ?Array { + return null; + } +} - const onDropLayer = React.useCallback( - (targetIndex: number) => { - const { current: draggedLayerIndex } = draggedLayerIndexRef; - if (draggedLayerIndex === null) return; +class LabelTreeViewItemContent implements TreeViewItemContent { + id: string; + label: string | React.Node; + dataSet: { [string]: string }; + rightButtons: Array; + buildMenuTemplateFunction: ( + i18n: I18nType, + index: number + ) => Array; - if (targetIndex !== draggedLayerIndex) { - layersContainer.moveLayer( - draggedLayerIndex, - targetIndex < draggedLayerIndex ? targetIndex + 1 : targetIndex - ); - triggerOnLayersModified(); - } - draggedLayerIndexRef.current = null; - }, - [layersContainer, triggerOnLayersModified] - ); + constructor( + id: string, + label: string | React.Node, + rightButtons: Array, + buildMenuTemplateFunction: ( + i18n: I18nType, + index: number + ) => Array + ) { + this.id = id; + this.label = label; + this.rightButtons = rightButtons; + this.buildMenuTemplateFunction = buildMenuTemplateFunction; + } - const layersCount = layersContainer.getLayersCount(); - const containerLayersList = mapReverseFor(0, layersCount, i => { - const layer = layersContainer.getLayerAt(i); - const layerName = layer.getName(); + getName(i18n: I18nType): string | React.Node { + return this.label; + } - return ( - onSelectLayer(layerName)} - nameError={nameErrors[layerName]} - effectsCount={getEffectsCount(project.getCurrentPlatform(), layer)} - onEditLayerEffects={() => onEditLayerEffects(layer)} - onEdit={() => onEdit(layer)} - onBeginDrag={() => { - draggedLayerIndexRef.current = i; - }} - onDrop={() => onDropLayer(i)} - onBlur={newName => { - setNameErrors(currentValue => ({ - ...currentValue, - [layerName]: null, - })); - - if (layerName === newName) return; - - const isNameAlreadyTaken = layersContainer.hasLayerNamed(newName); - if (isNameAlreadyTaken) { - setNameErrors(currentValue => ({ - ...currentValue, - [layerName]: The name {newName} is already taken, - })); - } else { - layersContainer.getLayer(layerName).setName(newName); - if (layout) { - gd.WholeProjectRefactorer.renameLayerInScene( - project, - layout, - layerName, - newName - ); - } else if (eventsFunctionsExtension && eventsBasedObject) { - gd.WholeProjectRefactorer.renameLayerInEventsBasedObject( - project, - eventsFunctionsExtension, - eventsBasedObject, - layerName, - newName - ); - } - onLayerRenamed(); - triggerOnLayersModified(); - } - }} - onRemove={() => { - onRemoveLayer(layerName, doRemove => { - if (!doRemove) return; + getId(): string { + return this.id; + } - layersContainer.removeLayer(layerName); - triggerOnLayersModified(); - }); - }} - isVisible={layer.getVisibility()} - onChangeVisibility={visible => { - layer.setVisibility(visible); - triggerOnLayersModified(); - }} - isLocked={layer.isLocked()} - onChangeLockState={isLocked => { - layer.setLocked(isLocked); - triggerOnLayersModified(); - }} - width={width} - /> - ); - }); + getRightButton(i18n: I18nType): Array { + return this.rightButtons; + } - return ( - - {containerLayersList} - true} - drop={() => { - onDropLayer(-1); - }} - > - {({ connectDropTarget, isOver, canDrop }) => - connectDropTarget( -
- {isOver && ( -
- )} - {layout && ( - - )} -
- ) - } - - - ); + getHtmlId(index: number): ?string { + return this.id; + } + + getDataSet(): ?HTMLDataset { + return null; + } + + getThumbnail(): ?string { + return null; + } + + onClick(): void {} + + buildMenuTemplate(i18n: I18nType, index: number) { + return this.buildMenuTemplateFunction(i18n, index); + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + + rename(newName: string): void {} + + edit(): void {} + + delete(): void {} + + copy(): void {} + + paste(): void {} + + cut(): void {} + + getIndex(): number { + return 0; + } + + moveAt(destinationIndex: number): void {} + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return false; + } + + getRootId(): string { + return ''; + } +} + +const getTreeViewItemName = (i18n: I18nType) => (item: TreeViewItem) => + item.content.getName(i18n); +const getTreeViewItemId = (item: TreeViewItem) => item.content.getId(); +const getTreeViewItemHtmlId = (item: TreeViewItem, index: number) => + item.content.getHtmlId(index); +const getTreeViewItemChildren = (i18n: I18nType) => (item: TreeViewItem) => + item.getChildren(i18n); +const getTreeViewItemThumbnail = (item: TreeViewItem) => + item.content.getThumbnail(); +const getTreeViewItemDataSet = (item: TreeViewItem) => + item.content.getDataSet(); +const buildMenuTemplate = (i18n: I18nType) => ( + item: TreeViewItem, + index: number +) => item.content.buildMenuTemplate(i18n, index); +const renderTreeViewItemRightComponent = (i18n: I18nType) => ( + item: TreeViewItem +) => item.content.renderRightComponent(i18n); +const renameItem = (item: TreeViewItem, newName: string) => { + item.content.rename(newName); }; +const onClickItem = (item: TreeViewItem) => { + item.content.onClick(); +}; +const editItem = (item: TreeViewItem) => { + item.content.edit(); +}; +const deleteItem = (item: TreeViewItem) => { + item.content.delete(); +}; +const getTreeViewItemRightButton = (i18n: I18nType) => (item: TreeViewItem) => + item.content.getRightButton(i18n); + +export type ProjectManagerInterface = {| + forceUpdateList: () => void, + focusSearchBar: () => void, +|}; type Props = {| project: gdProject, - selectedLayer: string, - onSelectLayer: string => void, + chosenLayer: string, + onChooseLayer: (layerName: string) => void, + onSelectLayer: (layer: gdLayer | null) => void, layout: gdLayout | null, eventsFunctionsExtension: gdEventsFunctionsExtension | null, eventsBasedObject: gdEventsBasedObject | null, @@ -242,7 +265,6 @@ type Props = {| onCreateLayer: () => void, onLayersVisibilityInEditorChanged: () => void, onBackgroundColorChanged: () => void, - unsavedChanges?: ?UnsavedChanges, gameEditorMode: 'embedded-game' | 'instances-editor', // Preview: @@ -250,134 +272,510 @@ type Props = {| |}; export type LayersListInterface = {| - forceUpdate: () => void, + forceUpdateList: () => void, |}; -const hasLightingLayer = (layersContainer: gdLayersContainer) => { - const layersCount = layersContainer.getLayersCount(); - return ( - mapReverseFor(0, layersCount, i => - layersContainer.getLayerAt(i).isLightingLayer() - ).filter(Boolean).length > 0 - ); -}; - const LayersList = React.forwardRef( - (props, ref) => { - const { eventsFunctionsExtension, eventsBasedObject, project } = props; + ( + { + project, + chosenLayer, + onChooseLayer, + onSelectLayer, + layout, + eventsFunctionsExtension, + eventsBasedObject, + layersContainer, + onEditLayerEffects, + onEditLayer, + onLayersModified, + onRemoveLayer, + onLayerRenamed, + onCreateLayer, + onLayersVisibilityInEditorChanged, + onBackgroundColorChanged, + gameEditorMode, + hotReloadPreviewButtonProps, + }, + ref + ) => { + const [selectedItems, setSelectedItems] = React.useState< + Array + >([]); + const unsavedChanges = React.useContext(UnsavedChangesContext); + const { triggerUnsavedChanges } = unsavedChanges; + const preferences = React.useContext(PreferencesContext); + const gdevelopTheme = React.useContext(GDevelopThemeContext); + const { currentlyRunningInAppTutorial } = React.useContext( + InAppTutorialContext + ); + const treeViewRef = React.useRef>(null); const forceUpdate = useForceUpdate(); + const { isMobile } = useResponsiveWindowSize(); + const { showDeleteConfirmation } = useAlertDialog(); + + const forceUpdateList = React.useCallback( + () => { + forceUpdate(); + if (treeViewRef.current) treeViewRef.current.forceUpdateList(); + }, + [forceUpdate] + ); + + const scrollToItem = React.useCallback((itemId: string) => { + if (treeViewRef.current) { + treeViewRef.current.scrollToItemFromId(itemId); + } + }, []); React.useImperativeHandle(ref, () => ({ - forceUpdate, + forceUpdateList: () => { + forceUpdate(); + if (treeViewRef.current) treeViewRef.current.forceUpdateList(); + }, })); - const addLayer = () => { - const { layersContainer } = props; - const name = newNameGenerator('Layer', name => - layersContainer.hasLayerNamed(name) - ); - layersContainer.insertNewLayer(name, layersContainer.getLayersCount()); - const newLayer = layersContainer.getLayer(name); - addDefaultLightToLayer(newLayer); - - onLayerModified(); - props.onCreateLayer(); - }; - - const addLightingLayer = () => { - const { layersContainer } = props; - const name = newNameGenerator('Lighting', name => - layersContainer.hasLayerNamed(name) - ); - layersContainer.insertNewLayer(name, layersContainer.getLayersCount()); - const layer = layersContainer.getLayer(name); - layer.setLightingLayer(true); - layer.setFollowBaseLayerCamera(true); - layer.setAmbientLightColor(200, 200, 200); - onLayerModified(); - props.onCreateLayer(); - }; - - const onLayerModified = () => { - if (props.unsavedChanges) props.unsavedChanges.triggerUnsavedChanges(); - props.onLayersModified(); - forceUpdate(); - }; - - // Force the list to be mounted again if layersContainer - // has been changed. Avoid accessing to invalid objects that could - // crash the app. - const listKey = props.layersContainer.ptr; - const isLightingLayerPresent = hasLightingLayer(props.layersContainer); + const editName = React.useCallback( + (itemId: string) => { + const treeView = treeViewRef.current; + if (treeView) { + if (isMobile) { + // Position item at top of the screen to make sure it will be visible + // once the keyboard is open. + treeView.scrollToItemFromId(itemId, 'start'); + } + treeView.renameItemFromId(itemId); + } + }, + [isMobile] + ); - return ( - - - - {({ width }) => ( - // TODO: The list is costly to render when there are many layers, consider - // using SortableVirtualizedItemList. - - )} - - - - {props.gameEditorMode === 'embedded-game' && ( - { - project.setEffectsHiddenInEditor( - !project.areEffectsHiddenInEditor() - ); - props.onLayersVisibilityInEditorChanged(); - forceUpdate(); - }} - selected={!project.areEffectsHiddenInEditor()} - tooltip={ - !project.areEffectsHiddenInEditor() - ? t`Disable effects/lighting in the editor` - : t`Display effects/lighting in the editor` - } - > - - - )} - Add a layer} - id="add-layer-button" - primary - onClick={addLayer} - icon={} - buildMenuTemplate={i18n => [ + const onTreeModified = React.useCallback( + (shouldForceUpdateList: boolean) => { + triggerUnsavedChanges(); + + if (shouldForceUpdateList) forceUpdateList(); + else forceUpdate(); + }, + [forceUpdate, forceUpdateList, triggerUnsavedChanges] + ); + + // Initialize keyboard shortcuts as empty. + // onDelete callback is set outside because it deletes the selected + // item (that is a props). As it is stored in a ref, the keyboard shortcut + // instance does not update with selectedItems changes. + const keyboardShortcutsRef = React.useRef( + new KeyboardShortcuts({ + shortcutCallbacks: {}, + }) + ); + React.useEffect( + () => { + if (keyboardShortcutsRef.current) { + keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => { + if (selectedItems.length > 0) { + deleteItem(selectedItems[0]); + } + }); + keyboardShortcutsRef.current.setShortcutCallback('onRename', () => { + if (selectedItems.length > 0) { + editName(selectedItems[0].content.getId()); + } + }); + } + }, + [editName, selectedItems] + ); + + const triggerOnLayersModified = React.useCallback( + () => { + onLayersModified(); + if (unsavedChanges) unsavedChanges.triggerUnsavedChanges(); + forceUpdate(); + }, + [forceUpdate, onLayersModified, unsavedChanges] + ); + + const triggerOnBackgroundColorChanged = React.useCallback( + () => { + onBackgroundColorChanged(); + if (unsavedChanges) unsavedChanges.triggerUnsavedChanges(); + forceUpdate(); + }, + [forceUpdate, onBackgroundColorChanged, unsavedChanges] + ); + + const onRenameLayer = React.useCallback( + (oldName: string, newName: string) => { + const uniqueNewName = newNameGenerator( + newName || 'Unnamed', + tentativeNewName => { + return layersContainer.hasLayerNamed(tentativeNewName); + } + ); + + layersContainer.getLayer(oldName).setName(uniqueNewName); + if (layout) { + gd.WholeProjectRefactorer.renameLayerInScene( + project, + layout, + oldName, + uniqueNewName + ); + } else if (eventsFunctionsExtension && eventsBasedObject) { + gd.WholeProjectRefactorer.renameLayerInEventsBasedObject( + project, + eventsFunctionsExtension, + eventsBasedObject, + oldName, + uniqueNewName + ); + } + onLayerRenamed(); + triggerOnLayersModified(); + }, + [ + eventsBasedObject, + eventsFunctionsExtension, + layersContainer, + layout, + onLayerRenamed, + project, + triggerOnLayersModified, + ] + ); + + const layerTreeViewItemProps = React.useMemo( + () => + project + ? { + project, + layersContainer, + chosenLayer, + onChooseLayer, + onSelectLayer, + onDeleteLayer: layer => { + const layerName = layer.getName(); + onRemoveLayer(layerName, doRemove => { + if (!doRemove) return; + + layersContainer.removeLayer(layerName); + triggerOnLayersModified(); + }); + }, + onEditLayer, + onLayersModified, + editName, + onRenameLayer, + forceUpdate, + forceUpdateList, + gdevelopTheme, + preferences, + scrollToItem, + showDeleteConfirmation, + triggerOnLayersModified, + } + : null, + [ + project, + layersContainer, + chosenLayer, + onChooseLayer, + onSelectLayer, + onEditLayer, + onLayersModified, + editName, + onRenameLayer, + forceUpdate, + forceUpdateList, + gdevelopTheme, + preferences, + scrollToItem, + showDeleteConfirmation, + triggerOnLayersModified, + onRemoveLayer, + ] + ); + + const onLayerModified = React.useCallback( + () => { + triggerUnsavedChanges(); + onLayersModified(); + forceUpdate(); + }, + [forceUpdate, onLayersModified, triggerUnsavedChanges] + ); + + const addLayer = React.useCallback( + () => { + const name = newNameGenerator('Layer', name => + layersContainer.hasLayerNamed(name) + ); + layersContainer.insertNewLayer(name, layersContainer.getLayersCount()); + const newLayer = layersContainer.getLayer(name); + addDefaultLightToLayer(newLayer); + onCreateLayer(); + onLayerModified(); + + const layerItemId = getLayerTreeViewItemId(newLayer); + if (treeViewRef.current) { + treeViewRef.current.openItems([layerItemId, layersRootFolderId]); + } + // We focus it so the user can edit the name directly. + editName(layerItemId); + + forceUpdateList(); + }, + [ + editName, + forceUpdateList, + layersContainer, + onCreateLayer, + onLayerModified, + ] + ); + + const addLightingLayer = React.useCallback( + () => { + const name = newNameGenerator('Lighting', name => + layersContainer.hasLayerNamed(name) + ); + layersContainer.insertNewLayer(name, layersContainer.getLayersCount()); + const layer = layersContainer.getLayer(name); + layer.setLightingLayer(true); + layer.setFollowBaseLayerCamera(true); + layer.setAmbientLightColor(200, 200, 200); + onCreateLayer(); + onLayerModified(); + forceUpdateList(); + }, + [forceUpdateList, layersContainer, onCreateLayer, onLayerModified] + ); + + const isLightingLayerPresent = hasLightingLayer(layersContainer); + + const getTreeViewData = React.useCallback( + (i18n: I18nType): Array => { + if (!project || !layerTreeViewItemProps) { + return []; + } + return [ + { + isRoot: false, + content: new LabelTreeViewItemContent( + layersRootFolderId, + '', + [ + gameEditorMode === 'embedded-game' + ? { + icon: !project.areEffectsHiddenInEditor() ? ( + + ) : ( + + ), + label: !project.areEffectsHiddenInEditor() + ? i18n._(t`Disable effects/lighting in the editor`) + : i18n._(t`Display effects/lighting in the editor`), + click: () => { + project.setEffectsHiddenInEditor( + !project.areEffectsHiddenInEditor() + ); + onLayersVisibilityInEditorChanged(); + forceUpdate(); + }, + id: 'show-effects-button', + } + : null, + { + icon: , + label: i18n._(t`Add a layer`), + click: addLayer, + id: 'add-layer-button', + }, + ].filter(Boolean), + () => + [ + gameEditorMode === 'embedded-game' + ? { + label: !project.areEffectsHiddenInEditor() + ? i18n._(t`Disable effects/lighting in the editor`) + : i18n._(t`Display effects/lighting in the editor`), + click: () => { + project.setEffectsHiddenInEditor( + !project.areEffectsHiddenInEditor() + ); + onLayersVisibilityInEditorChanged(); + forceUpdate(); + }, + } + : null, + { + label: i18n._(t`Add a layer`), + click: addLayer, + }, { label: i18n._(t`Add 2D lighting layer`), enabled: !isLightingLayerPresent, click: addLightingLayer, }, - ]} - /> - - - + ].filter(Boolean) + ), + getChildren(i18n: I18nType): ?Array { + return null; + }, + }, + ...mapReverseFor( + 0, + layersContainer.getLayersCount(), + i => + new LeafTreeViewItem( + new LayerTreeViewItemContent( + layersContainer.getLayerAt(i), + layerTreeViewItemProps + ) + ) + ), + layout + ? { + isRoot: false, + content: new BackgroundColorTreeViewItemContent( + layout, + triggerOnBackgroundColorChanged + ), + getChildren(i18n: I18nType): ?Array { + return null; + }, + } + : null, + ].filter(Boolean); + }, + [ + project, + layerTreeViewItemProps, + gameEditorMode, + addLayer, + layout, + onLayersVisibilityInEditorChanged, + forceUpdate, + isLightingLayerPresent, + addLightingLayer, + layersContainer, + triggerOnBackgroundColorChanged, + ] + ); + + const canMoveSelectionTo = React.useCallback( + (destinationItem: TreeViewItem, where: 'before' | 'inside' | 'after') => + selectedItems.every(item => { + return ( + item.content.getRootId().length > 0 && + item.content.getRootId() === destinationItem.content.getRootId() + ); + }), + [selectedItems] + ); + + const moveSelectionTo = React.useCallback( + ( + i18n: I18nType, + destinationItem: TreeViewItem, + where: 'before' | 'inside' | 'after' + ) => { + if (selectedItems.length === 0) { + return; + } + const selectedItem = selectedItems[0]; + selectedItem.content.moveAt( + destinationItem.content.getIndex() + (where === 'after' ? 1 : 0) + ); + onTreeModified(true); + }, + [onTreeModified, selectedItems] + ); + + /** + * Unselect item if one of the parent is collapsed (folded) so that the item + * does not stay selected and not visible to the user. + */ + const onCollapseItem = React.useCallback( + (item: TreeViewItem) => { + if (selectedItems.length !== 1 || item.isPlaceholder) { + return; + } + if (selectedItems[0].content.isDescendantOf(item.content)) { + setSelectedItems([]); + } + }, + [selectedItems] + ); + + // Force List component to be mounted again if project + // has been changed. Avoid accessing to invalid objects that could + // crash the app. + const listKey = project ? project.ptr : 'no-project'; + const initiallyOpenedNodeIds = [layersRootFolderId]; + + return ( + + + {({ i18n }) => ( +
+ + {({ height }) => ( + { + const itemToSelect = items[0]; + if (!itemToSelect) return; + if (itemToSelect.isRoot) return; + setSelectedItems(items); + }} + onClickItem={onClickItem} + onRenameItem={renameItem} + buildMenuTemplate={buildMenuTemplate(i18n)} + getItemRightButton={getTreeViewItemRightButton(i18n)} + renderRightComponent={renderTreeViewItemRightComponent( + i18n + )} + onMoveSelectionToItem={(destinationItem, where) => + moveSelectionTo(i18n, destinationItem, where) + } + canMoveSelectionToItem={canMoveSelectionTo} + reactDndType={extensionItemReactDndType} + initiallyOpenedNodeIds={initiallyOpenedNodeIds} + forceDefaultDraggingPreview + shouldHideMenuIcon={item => + item.content.getId() === layersRootFolderId || + item.content.getId() === backgroundColorId + } + /> + )} + +
+ )} +
); } diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js index a66acfe7b204..0235b95c948d 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js @@ -26,8 +26,6 @@ import Paper from '../../UI/Paper'; import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout'; import { IconContainer } from '../../UI/IconContainer'; import RemoveIcon from '../../UI/CustomSvgIcons/Remove'; -import VisibilityIcon from '../../UI/CustomSvgIcons/Visibility'; -import VisibilityOffIcon from '../../UI/CustomSvgIcons/VisibilityOff'; import useForceUpdate, { useForceRecompute } from '../../Utils/UseForceUpdate'; import ChevronArrowTop from '../../UI/CustomSvgIcons/ChevronArrowTop'; import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight'; @@ -40,12 +38,7 @@ import Edit from '../../UI/CustomSvgIcons/ShareExternal'; import { useManageObjectBehaviors } from '../../BehaviorsEditor'; import Object3d from '../../UI/CustomSvgIcons/Object3d'; import Object2d from '../../UI/CustomSvgIcons/Object2d'; -import { CompactEffectPropertiesEditor } from '../../EffectsList/CompactEffectPropertiesEditor'; import { mapFor } from '../../Utils/MapFor'; -import { - getEnumeratedEffectMetadata, - useManageEffects, -} from '../../EffectsList'; import CompactSelectField from '../../UI/CompactSelectField'; import SelectOption from '../../UI/SelectOption'; import { ChildObjectPropertiesEditor } from './ChildObjectPropertiesEditor'; @@ -67,6 +60,7 @@ import { import NewVariantDialog from '../Editors/CustomObjectPropertiesEditor/NewVariantDialog'; import useAlertDialog from '../../UI/Alert/useAlertDialog'; import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow'; +import { CompactEffectsListEditor } from '../../LayersList/CompactLayerPropertiesEditor/CompactEffectsListEditor'; const gd: libGDevelop = global.gd; @@ -93,7 +87,6 @@ export const styles = { }; const behaviorsHelpLink = getHelpLink('/behaviors'); -const effectsHelpLink = getHelpLink('/objects/effects'); const objectVariablesHelpLink = getHelpLink( '/all-features/variables/object-variables' ); @@ -291,7 +284,6 @@ export const CompactObjectPropertiesEditor = ({ const [isPropertiesFolded, setIsPropertiesFolded] = React.useState(false); const [isBehaviorsFolded, setIsBehaviorsFolded] = React.useState(false); const [isVariablesFolded, setIsVariablesFolded] = React.useState(false); - const [isEffectsFolded, setIsEffectsFolded] = React.useState(false); const [newVariantDialogOpen, setNewVariantDialogOpen] = React.useState(false); const [ duplicateAndEditVariantDialogOpen, @@ -407,26 +399,6 @@ export const CompactObjectPropertiesEditor = ({ .map(behaviorName => object.getBehavior(behaviorName)) .filter(behavior => !behavior.isDefaultBehavior()); - // Effects: - const effectsContainer = object.getEffects(); - const { - allEffectMetadata, - all2DEffectMetadata, - addEffect, - removeEffect, - chooseEffectType, - } = useManageEffects({ - effectsContainer, - project, - onEffectsUpdated: () => { - onObjectsModified([object]); - forceUpdate(); - }, - onEffectAdded, - onUpdate: forceUpdate, - target: 'object', - }); - // Events based object children: const customObjectEventsBasedObject = project.hasEventsBasedObject( objectConfiguration.getType() @@ -948,119 +920,20 @@ export const CompactObjectPropertiesEditor = ({ objectMetadata.hasDefaultBehavior( 'EffectCapability::EffectBehavior' ) && ( - Effects} - isFolded={isEffectsFolded} - toggleFolded={() => setIsEffectsFolded(!isEffectsFolded)} + onObjectsModified([object])} onOpenFullEditor={() => onEditObject(object, 'effects')} - onAdd={() => addEffect(false)} - renderContent={() => ( - - {effectsContainer.getEffectsCount() === 0 && ( - - - There are no{' '} - - Window.openExternalURL(effectsHelpLink) - } - > - effects - {' '} - on this object. - - - )} - {mapFor( - 0, - effectsContainer.getEffectsCount(), - (index: number) => { - const effect: gdEffect = effectsContainer.getEffectAt( - index - ); - const effectType = effect.getEffectType(); - const effectMetadata = getEnumeratedEffectMetadata( - allEffectMetadata, - effectType - ); - - return ( - ( - - - chooseEffectType(effect, type) - } - > - {all2DEffectMetadata.map(effectMetadata => ( - - ))} - - - onObjectsModified([object]) - } - /> - - )} - isFolded={effect.isFolded()} - toggleFolded={() => { - effect.setFolded(!effect.isFolded()); - forceUpdate(); - }} - title={effect.getName()} - titleBarButtons={[ - { - id: 'effect-visibility', - icon: effect.isEnabled() - ? VisibilityIcon - : VisibilityOffIcon, - label: effect.isEnabled() - ? t`Hide effect` - : t`Show effect`, - onClick: () => { - effect.setEnabled(!effect.isEnabled()); - onObjectsModified([object]); - forceUpdate(); - }, - }, - { - id: 'remove-effect', - icon: RemoveIcon, - label: t`Remove effect`, - onClick: () => { - removeEffect(effect); - onObjectsModified([object]); - }, - }, - ]} - /> - ); - } - )} - - )} + onEffectAdded={onEffectAdded} /> )} diff --git a/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js b/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js index cddd0216bd37..b025989e3ea7 100644 --- a/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js +++ b/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js @@ -241,7 +241,12 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent { ); } - onClick(): void {} + onClick(): void { + this.props.selectObjectFolderOrObjectWithContext({ + objectFolderOrObject: this.object, + global: this._isGlobal, + }); + } rename(newName: string): void { if (this.getName() === newName) { diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js index 3a59b1979b33..475b3012d65c 100644 --- a/newIDE/app/src/ObjectsList/index.js +++ b/newIDE/app/src/ObjectsList/index.js @@ -384,6 +384,9 @@ const renderTreeViewItemRightComponent = (i18n: I18nType) => ( const renameItem = (item: TreeViewItem, newName: string) => { item.content.rename(newName); }; +const onClickItem = (item: TreeViewItem) => { + item.content.onClick(); +}; const editItem = (item: TreeViewItem) => { item.content.edit(); }; @@ -1575,6 +1578,7 @@ const ObjectsList = React.forwardRef( getItemHtmlId={getTreeViewItemHtmlId} getItemDataset={getTreeViewItemDataSet} onEditItem={editItem} + onClickItem={onClickItem} onCollapseItem={onCollapseItem} selectedItems={selectedItems} onSelectItems={items => { diff --git a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js index 6f068fe7b713..e4923b599c7e 100644 --- a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js +++ b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js @@ -33,9 +33,8 @@ export type SceneEditorsDisplayProps = {| objectsContainer: gdObjectsContainer, projectScopedContainersAccessor: ProjectScopedContainersAccessor, initialInstances: gdInitialInstancesContainer, - lastSelectionType: 'instance' | 'object', + lastSelectionType: 'instance' | 'object' | 'layer', instancesSelection: InstancesSelection, - selectedLayer: string, onSelectInstances: ( instances: Array, multiSelect: boolean, @@ -62,7 +61,10 @@ export type SceneEditorsDisplayProps = {| variant: gdEventsBasedObjectVariant ) => void, selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[], - onSelectLayer: (layerName: string) => void, + chosenLayer: string, + onChooseLayer: (layerName: string) => void, + selectedLayer: gdLayer | null, + onSelectLayer: (layer: gdLayer | null) => void, editLayerEffects: (layer: ?gdLayer) => void, editLayer: (layer: ?gdLayer) => void, onRemoveLayer: (layerName: string, done: (boolean) => void) => void, diff --git a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js index f4c186c0fc1e..59e0a437226f 100644 --- a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js +++ b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js @@ -13,6 +13,7 @@ import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer' import { CompactObjectPropertiesEditor } from '../ObjectEditor/CompactObjectPropertiesEditor'; import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; +import { CompactLayerPropertiesEditor } from '../LayersList/CompactLayerPropertiesEditor'; export const styles = { paper: { @@ -26,17 +27,18 @@ export const styles = { type Props = {| project: gdProject, resourceManagementProps: ResourceManagementProps, - layout?: ?gdLayout, - eventsFunctionsExtension: gdEventsFunctionsExtension | null, - objectsContainer: gdObjectsContainer, - globalObjectsContainer: gdObjectsContainer | null, layersContainer: gdLayersContainer, projectScopedContainersAccessor: ProjectScopedContainersAccessor, unsavedChanges?: ?UnsavedChanges, i18n: I18nType, + lastSelectionType: 'instance' | 'object' | 'layer', + + // For objects or instances: historyHandler?: HistoryHandler, - lastSelectionType: 'instance' | 'object', isVariableListLocked: boolean, + layout?: ?gdLayout, + objectsContainer: gdObjectsContainer, + globalObjectsContainer: gdObjectsContainer | null, // For objects: objects: Array, @@ -57,6 +59,7 @@ type Props = {| variant: gdEventsBasedObjectVariant ) => void, isBehaviorListLocked: boolean, + eventsFunctionsExtension: gdEventsFunctionsExtension | null, // For instances: instances: Array, @@ -66,6 +69,12 @@ type Props = {| editInstanceVariables: gdInitialInstance => void, tileMapTileSelection: ?TileMapTileSelection, onSelectTileMapTile: (?TileMapTileSelection) => void, + + // For layers: + layer: gdLayer | null, + onEditLayer: (layer: gdLayer) => void, + onEditLayerEffects: (layer: gdLayer) => void, + onLayersModified: (layers: Array) => void, |}; export type InstanceOrObjectPropertiesEditorInterface = {| @@ -113,6 +122,20 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef< editInstanceVariables, tileMapTileSelection, onSelectTileMapTile, + + // For layers + layer, + onEditLayer, + onEditLayerEffects, + onLayersModified, + + // For objects or instances: + historyHandler, + isVariableListLocked, + layout, + objectsContainer, + globalObjectsContainer, + ...commonProps } = props; @@ -127,6 +150,11 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef< editInstanceVariables={editInstanceVariables} tileMapTileSelection={tileMapTileSelection} onSelectTileMapTile={onSelectTileMapTile} + historyHandler={historyHandler} + isVariableListLocked={isVariableListLocked} + layout={layout} + objectsContainer={objectsContainer} + globalObjectsContainer={globalObjectsContainer} {...commonProps} /> ) : !!objects.length && lastSelectionType === 'object' ? ( @@ -145,6 +173,21 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef< onOpenEventBasedObjectVariantEditor } onDeleteEventsBasedObjectVariant={onDeleteEventsBasedObjectVariant} + historyHandler={historyHandler} + isVariableListLocked={isVariableListLocked} + layout={layout} + objectsContainer={objectsContainer} + globalObjectsContainer={globalObjectsContainer} + {...commonProps} + /> + ) : layer && lastSelectionType === 'layer' ? ( + ) : ( diff --git a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js index 01f6ef6c8761..92aad93b6ed2 100644 --- a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js @@ -86,6 +86,7 @@ const MosaicEditorsDisplay = React.forwardRef< objectsContainer, projectScopedContainersAccessor, initialInstances, + chosenLayer, selectedLayer, onSelectInstances, onInstancesModified, @@ -135,7 +136,7 @@ const MosaicEditorsDisplay = React.forwardRef< [] ); const forceUpdateLayersList = React.useCallback(() => { - if (layersListRef.current) layersListRef.current.forceUpdate(); + if (layersListRef.current) layersListRef.current.forceUpdateList(); }, []); const getInstanceSize = React.useCallback((instance: gdInitialInstance) => { return editorRef.current @@ -290,6 +291,7 @@ const MosaicEditorsDisplay = React.forwardRef< projectScopedContainersAccessor={projectScopedContainersAccessor} instances={selectedInstances} objects={selectedObjects} + layer={selectedLayer} editInstanceVariables={props.editInstanceVariables} editObjectInPropertiesPanel={props.editObjectInPropertiesPanel} onEditObject={props.onEditObject} @@ -313,6 +315,9 @@ const MosaicEditorsDisplay = React.forwardRef< } isVariableListLocked={isCustomVariant} isBehaviorListLocked={isCustomVariant} + onEditLayerEffects={props.editLayerEffects} + onEditLayer={props.editLayer} + onLayersModified={props.onLayersModified} /> )} @@ -327,7 +332,8 @@ const MosaicEditorsDisplay = React.forwardRef< layout={layout} eventsFunctionsExtension={eventsFunctionsExtension} eventsBasedObject={eventsBasedObject} - selectedLayer={selectedLayer} + chosenLayer={chosenLayer} + onChooseLayer={props.onChooseLayer} onSelectLayer={props.onSelectLayer} onEditLayerEffects={props.editLayerEffects} onEditLayer={props.editLayer} @@ -339,7 +345,6 @@ const MosaicEditorsDisplay = React.forwardRef< onLayerRenamed={props.onLayerRenamed} onCreateLayer={forceUpdatePropertiesEditor} layersContainer={layersContainer} - unsavedChanges={props.unsavedChanges} ref={layersListRef} hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps} onBackgroundColorChanged={props.onBackgroundColorChanged} @@ -389,7 +394,7 @@ const MosaicEditorsDisplay = React.forwardRef< globalObjectsContainer={globalObjectsContainer} objectsContainer={objectsContainer} layersContainer={layersContainer} - selectedLayer={selectedLayer} + chosenLayer={chosenLayer} initialInstances={initialInstances} instancesEditorSettings={props.instancesEditorSettings} onInstancesEditorSettingsMutated={ diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js index a3d70fe13507..1ee17e9572e3 100644 --- a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js @@ -78,6 +78,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< objectsContainer, projectScopedContainersAccessor, initialInstances, + chosenLayer, selectedLayer, onSelectInstances, onInstancesModified, @@ -151,7 +152,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< [] ); const forceUpdateLayersList = React.useCallback(() => { - if (layersListRef.current) layersListRef.current.forceUpdate(); + if (layersListRef.current) layersListRef.current.forceUpdateList(); }, []); const getInstanceSize = React.useCallback((instance: gdInitialInstance) => { return editorRef.current @@ -326,7 +327,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< globalObjectsContainer={globalObjectsContainer} objectsContainer={objectsContainer} layersContainer={layersContainer} - selectedLayer={selectedLayer} + chosenLayer={chosenLayer} screenType={screenType} initialInstances={initialInstances} instancesEditorSettings={props.instancesEditorSettings} @@ -453,6 +454,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< } objects={selectedObjects} instances={selectedInstances} + layer={selectedLayer} editInstanceVariables={props.editInstanceVariables} editObjectInPropertiesPanel={ props.editObjectInPropertiesPanel @@ -463,7 +465,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< onInstancesModified={forceUpdateInstancesList} onGetInstanceSize={getInstanceSize} ref={instanceOrObjectPropertiesEditorRef} - unsavedChanges={props.unsavedChanges} historyHandler={props.historyHandler} tileMapTileSelection={props.tileMapTileSelection} onSelectTileMapTile={props.onSelectTileMapTile} @@ -478,6 +479,9 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< } isVariableListLocked={isCustomVariant} isBehaviorListLocked={isCustomVariant} + onEditLayerEffects={props.editLayerEffects} + onEditLayer={props.editLayer} + onLayersModified={props.onLayersModified} /> )} @@ -536,7 +540,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< layout={layout} eventsFunctionsExtension={eventsFunctionsExtension} eventsBasedObject={eventsBasedObject} - selectedLayer={selectedLayer} + chosenLayer={chosenLayer} + onChooseLayer={props.onChooseLayer} onSelectLayer={props.onSelectLayer} onEditLayerEffects={props.editLayerEffects} onLayersModified={props.onLayersModified} @@ -548,7 +553,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< onLayerRenamed={props.onLayerRenamed} onCreateLayer={forceUpdatePropertiesEditor} layersContainer={layersContainer} - unsavedChanges={props.unsavedChanges} ref={layersListRef} hotReloadPreviewButtonProps={ props.hotReloadPreviewButtonProps diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js index 9248c50b492a..3401c35ccabe 100644 --- a/newIDE/app/src/SceneEditor/index.js +++ b/newIDE/app/src/SceneEditor/index.js @@ -279,11 +279,12 @@ type State = {| additionalWorkInfoBar: InfoBarDetails, selectedObjectFolderOrObjectsWithContext: Array, - selectedLayer: string, + chosenLayer: string, + selectedLayer: gdLayer | null, tileMapTileSelection: ?TileMapTileSelection, - lastSelectionType: 'instance' | 'object', + lastSelectionType: 'instance' | 'object' | 'layer', |}; type CopyCutPasteOptions = {| @@ -342,8 +343,9 @@ export default class SceneEditor extends React.Component { tileMapTileSelection: null, selectedObjectFolderOrObjectsWithContext: [], - selectedLayer: + chosenLayer: initialInstancesEditorSettings.selectedLayer || BASE_LAYER_NAME, + selectedLayer: null, invisibleLayerOnWhichInstancesHaveJustBeenAdded: null, lastSelectionType: 'instance', @@ -1071,7 +1073,7 @@ export default class SceneEditor extends React.Component { const instances = this.editorDisplay.instancesHandlers.addInstances( pos, [objectName], - this.state.selectedLayer + this.state.chosenLayer ); this._onInstancesAddedAndSendToEditor3D(instances); }; @@ -1454,11 +1456,11 @@ export default class SceneEditor extends React.Component { _onRemoveLayer = (layerName: string, done: boolean => void) => { const getNewState = (doRemove: boolean) => { - const newState: {| layerRemoved: null, selectedLayer?: string |} = { + const newState: {| layerRemoved: null, chosenLayer?: string |} = { layerRemoved: null, }; - if (doRemove && layerName === this.state.selectedLayer) { - newState.selectedLayer = BASE_LAYER_NAME; + if (doRemove && layerName === this.state.chosenLayer) { + newState.chosenLayer = BASE_LAYER_NAME; } return newState; }; @@ -1583,13 +1585,9 @@ export default class SceneEditor extends React.Component { this._sendHotReloadLayers(); }; - _onSelectLayer = (layerName: string) => { + _onChooseLayer = (layerName: string) => { this.setState({ - selectedLayer: layerName, - instancesEditorSettings: { - ...this.state.instancesEditorSettings, - selectedLayer: layerName, - }, + chosenLayer: layerName, }); const { previewDebuggerServer } = this.props; @@ -1607,6 +1605,13 @@ export default class SceneEditor extends React.Component { } }; + _onSelectLayer = (layer: gdLayer | null) => { + this.setState({ + selectedLayer: layer, + lastSelectionType: 'layer', + }); + }; + _onDeleteObjects = ( i18n: I18nType, objectsWithContext: ObjectWithContext[], @@ -2581,8 +2586,8 @@ export default class SceneEditor extends React.Component { forceUpdateLayersList = () => { // The selected layer could have been deleted when editing a linked external layout. - if (!this.props.layersContainer.hasLayerNamed(this.state.selectedLayer)) { - this.setState({ selectedLayer: BASE_LAYER_NAME }); + if (!this.props.layersContainer.hasLayerNamed(this.state.chosenLayer)) { + this.setState({ chosenLayer: BASE_LAYER_NAME }); } if (this.editorDisplay) this.editorDisplay.forceUpdateLayersList(); }; @@ -2746,7 +2751,10 @@ export default class SceneEditor extends React.Component { onSelectInstances={this._onSelectInstances} onInstancesModified={this._onInstancesModified} onAddObjectInstance={this.addInstanceOnTheScene} + chosenLayer={this.state.chosenLayer} + onChooseLayer={this._onChooseLayer} selectedLayer={this.state.selectedLayer} + onSelectLayer={this._onSelectLayer} editLayer={this.editLayer} editLayerEffects={this.editLayerEffects} editInstanceVariables={this.editInstanceVariables} @@ -2762,7 +2770,6 @@ export default class SceneEditor extends React.Component { this._onLayersVisibilityInEditorChanged } onRemoveLayer={this._onRemoveLayer} - onSelectLayer={this._onSelectLayer} tileMapTileSelection={this.state.tileMapTileSelection} onSelectTileMapTile={this.onSelectTileMapTile} onExportAssets={this.openObjectExporterDialog} diff --git a/newIDE/app/src/UI/CustomSvgIcons/LightbulbOff.js b/newIDE/app/src/UI/CustomSvgIcons/LightbulbOff.js new file mode 100644 index 000000000000..3bc4ed95edaa --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/LightbulbOff.js @@ -0,0 +1,13 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/LightbulbOn.js b/newIDE/app/src/UI/CustomSvgIcons/LightbulbOn.js new file mode 100644 index 000000000000..3048caa13001 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/LightbulbOn.js @@ -0,0 +1,13 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + +)); diff --git a/newIDE/app/src/UI/ErrorBoundary.js b/newIDE/app/src/UI/ErrorBoundary.js index 8f3e3c473488..81316f9383ff 100644 --- a/newIDE/app/src/UI/ErrorBoundary.js +++ b/newIDE/app/src/UI/ErrorBoundary.js @@ -46,6 +46,7 @@ type ErrorBoundaryScope = | 'scene-editor' | 'scene-editor-instance-properties' | 'scene-editor-object-properties' + | 'scene-editor-layer-properties' | 'scene-editor-objects-list' | 'scene-editor-object-groups-list' | 'scene-editor-canvas' diff --git a/newIDE/app/src/UI/TreeView/TreeViewRow.js b/newIDE/app/src/UI/TreeView/TreeViewRow.js index f4a929e006c6..9d637b763c41 100644 --- a/newIDE/app/src/UI/TreeView/TreeViewRow.js +++ b/newIDE/app/src/UI/TreeView/TreeViewRow.js @@ -420,12 +420,16 @@ const TreeViewRow = (props: Props) => { itemRow = connectDragPreview(itemRow); } - const rightButton = node.rightButton; + const rightButtons = Array.isArray(node.rightButton) + ? node.rightButton + : node.rightButton + ? [node.rightButton] + : []; const shouldDisplayMenu = !node.shouldHideMenuIcon && !isMobile && - !node.item.isRoot && + (!node.item.isRoot || node.shouldHideMenuIcon !== null) && !node.item.isPlaceholder; const dragSource = connectDragSource( @@ -446,7 +450,9 @@ const TreeViewRow = (props: Props) => { {...longTouchForContextMenuProps} > {itemRow} - {(node.rightComponent || rightButton || shouldDisplayMenu) && ( + {(node.rightComponent || + rightButtons.length > 0 || + shouldDisplayMenu) && (
(props: Props) => { )} > {node.rightComponent} - {rightButton && - (rightButton.primary ? ( + {rightButtons.map(rightButton => + rightButton.primary ? ( { e.stopPropagation(); if (rightButton.click) { @@ -479,6 +486,7 @@ const TreeViewRow = (props: Props) => { ) : ( { e.stopPropagation(); @@ -495,7 +503,8 @@ const TreeViewRow = (props: Props) => { > {rightButton.icon} - ))} + ) + )} {shouldDisplayMenu && ( = {| id: string, name: string | React.Node, rightComponent: ?React.Node, - rightButton: ?MenuButton, - shouldHideMenuIcon: boolean, + rightButton: ?MenuButton | Array, + shouldHideMenuIcon: boolean | null, hasChildren: boolean, canHaveChildren: boolean, extraClass: string, @@ -149,7 +149,7 @@ type Props = {| getItemDataset?: Item => ?HTMLDataset, onEditItem?: Item => void, buildMenuTemplate: (Item, index: number) => any, - getItemRightButton?: Item => ?MenuButton, + getItemRightButton?: Item => ?MenuButton | Array, renderRightComponent?: Item => ?React.Node, /** * Callback called when a folder is collapsed (folded). @@ -297,7 +297,7 @@ const InnerTreeView = ( rightButton, shouldHideMenuIcon: shouldHideMenuIcon ? shouldHideMenuIcon(item) - : false, + : null, hasChildren: !!children && children.length > 0, canHaveChildren, depth, diff --git a/newIDE/app/src/fixtures/TestProject.js b/newIDE/app/src/fixtures/TestProject.js index 7d5ebea67a4e..5e3db53e0215 100644 --- a/newIDE/app/src/fixtures/TestProject.js +++ b/newIDE/app/src/fixtures/TestProject.js @@ -50,6 +50,7 @@ export type TestProject = {| layerWith2DEffects: gdLayer, layerWithEffectWithoutEffectType: gdLayer, layerWithoutEffects: gdLayer, + lightingLayer: gdLayer, spriteObjectWithEffects: gdObject, spriteObjectWithoutEffects: gdObject, stringRelationalOperatorParameterMetadata: gdParameterMetadata, @@ -855,6 +856,12 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => { const layerWithoutEffects = new gd.Layer(); + const lightingLayer = new gd.Layer(); + lightingLayer.setName('Lighting'); + lightingLayer.setLightingLayer(true); + lightingLayer.setFollowBaseLayerCamera(true); + lightingLayer.setAmbientLightColor(200, 200, 200); + { const effect1 = spriteObjectWithEffects .getEffects() @@ -983,6 +990,7 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => { layerWith2DEffects, layerWithEffectWithoutEffectType, layerWithoutEffects, + lightingLayer, spriteObjectWithEffects, spriteObjectWithoutEffects, stringRelationalOperatorParameterMetadata, diff --git a/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js b/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js index 49847c7ff8d7..2c67f7976f1b 100644 --- a/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js +++ b/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js @@ -46,7 +46,7 @@ export const Default = () => ( layersContainer={testProject.testLayout.getLayers()} globalObjectsContainer={testProject.project.getObjects()} objectsContainer={testProject.testLayout.getObjects()} - selectedLayer={''} + chosenLayer={''} initialInstances={testProject.testLayout.getInitialInstances()} instancesEditorSettings={instancesEditorSettings} onInstancesEditorSettingsMutated={() => {}} diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactLayerPropertiesEditor.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactLayerPropertiesEditor.stories.js new file mode 100644 index 000000000000..d8d79dbf3fa4 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactLayerPropertiesEditor.stories.js @@ -0,0 +1,65 @@ +// @flow + +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; +import { I18n } from '@lingui/react'; + +// Keep first as it creates the `global.gd` object: +import { testProject } from '../../GDevelopJsInitializerDecorator'; + +import paperDecorator from '../../PaperDecorator'; +import { CompactLayerPropertiesEditor } from '../../../LayersList/CompactLayerPropertiesEditor'; +import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider'; +import fakeResourceManagementProps from '../../FakeResourceManagement'; + +export default { + title: 'LayoutEditor/CompactLayerPropertiesEditor', + component: CompactLayerPropertiesEditor, + decorators: [paperDecorator], +}; + +export const Layer = () => ( + + + {({ i18n }) => ( + + )} + + +); + +export const LightingLayer = () => ( + + + {({ i18n }) => ( + + )} + + +); diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactObjectPropertiesEditor.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactObjectPropertiesEditor.stories.js new file mode 100644 index 000000000000..7af8bd9b2dce --- /dev/null +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactObjectPropertiesEditor.stories.js @@ -0,0 +1,176 @@ +// @flow + +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; +import { I18n } from '@lingui/react'; + +// Keep first as it creates the `global.gd` object: +import { testProject } from '../../GDevelopJsInitializerDecorator'; + +import paperDecorator from '../../PaperDecorator'; +import { CompactObjectPropertiesEditor } from '../../../ObjectEditor/CompactObjectPropertiesEditor'; +import SerializedObjectDisplay from '../../SerializedObjectDisplay'; +import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider'; +import fakeResourceManagementProps from '../../FakeResourceManagement'; + +export default { + title: 'LayoutEditor/CompactObjectPropertiesEditor', + component: CompactObjectPropertiesEditor, + decorators: [paperDecorator], +}; + +export const Sprite2d = () => ( + + + {({ i18n }) => ( + + + + )} + + +); + +export const Cube3d = () => ( + + + {({ i18n }) => ( + + + + )} + + +); + +export const TextInput = () => ( + + + {({ i18n }) => ( + + + + )} + + +); + +export const CustomObject = () => ( + + + {({ i18n }) => ( + + + + )} + + +); diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/LayersList.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/LayersList.stories.js index feda75ccfeca..2bf3a0df5687 100644 --- a/newIDE/app/src/stories/componentStories/LayoutEditor/LayersList.stories.js +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/LayersList.stories.js @@ -18,38 +18,41 @@ export default { }; export const Default = () => { - const [selectedLayer, setSelectedLayer] = React.useState(''); + const [chosenLayer, setChosenLayer] = React.useState(''); return ( - { - cb(true); - }} - onLayerRenamed={action('onLayerRenamed')} - onCreateLayer={action('onCreateLayer')} - layout={testProject.testLayout} - layersContainer={testProject.testLayout.getLayers()} - hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps} - onBackgroundColorChanged={action('onBackgroundColorChanged')} - gameEditorMode={'embedded-game'} - /> +
+ { + cb(true); + }} + onLayerRenamed={action('onLayerRenamed')} + onCreateLayer={action('onCreateLayer')} + layout={testProject.testLayout} + layersContainer={testProject.testLayout.getLayers()} + hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps} + onBackgroundColorChanged={action('onBackgroundColorChanged')} + gameEditorMode={'embedded-game'} + /> +
); }; export const SmallWidthAndHeight = () => { - const [selectedLayer, setSelectedLayer] = React.useState(''); + const [chosenLayer, setChosenLayer] = React.useState(''); return ( @@ -58,8 +61,9 @@ export const SmallWidthAndHeight = () => { project={testProject.project} eventsFunctionsExtension={null} eventsBasedObject={null} - selectedLayer={selectedLayer} - onSelectLayer={setSelectedLayer} + chosenLayer={chosenLayer} + onChooseLayer={setChosenLayer} + onSelectLayer={action('onSelectLayer')} onEditLayerEffects={action('onEditLayerEffects')} onLayersModified={action('onLayersModified')} onLayersVisibilityInEditorChanged={action(