diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 88fef67792ac..d618dad7ab16 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -846,6 +846,9 @@ interface BehaviorsSharedData { boolean UpdateProperty([Const] DOMString name, [Const] DOMString value); void InitializeContent(); + boolean IsFolded(); + void SetFolded(boolean folded); + [Ref] QuickCustomizationVisibilitiesContainer GetPropertiesQuickCustomizationVisibilities(); }; diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index b0eeb72e7dac..39eae72e17e9 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -742,6 +742,8 @@ export class BehaviorsSharedData extends EmscriptenObject { getProperties(): MapStringPropertyDescriptor; updateProperty(name: string, value: string): boolean; initializeContent(): void; + isFolded(): boolean; + setFolded(folded: boolean): void; getPropertiesQuickCustomizationVisibilities(): QuickCustomizationVisibilitiesContainer; } diff --git a/GDevelop.js/types/gdbehaviorsshareddata.js b/GDevelop.js/types/gdbehaviorsshareddata.js index 1cdf78cfb382..f7c1821a3e88 100644 --- a/GDevelop.js/types/gdbehaviorsshareddata.js +++ b/GDevelop.js/types/gdbehaviorsshareddata.js @@ -7,6 +7,8 @@ declare class gdBehaviorsSharedData { getProperties(): gdMapStringPropertyDescriptor; updateProperty(name: string, value: string): boolean; initializeContent(): void; + isFolded(): boolean; + setFolded(folded: boolean): void; getPropertiesQuickCustomizationVisibilities(): gdQuickCustomizationVisibilitiesContainer; delete(): void; ptr: number; diff --git a/newIDE/app/src/EventsSheet/index.js b/newIDE/app/src/EventsSheet/index.js index 663cd4b84107..50d4ef350a8f 100644 --- a/newIDE/app/src/EventsSheet/index.js +++ b/newIDE/app/src/EventsSheet/index.js @@ -541,7 +541,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component< onToggleSearchPanel={this._toggleSearchPanel} canMoveEventsIntoNewGroup={hasSomethingSelected(this.state.selection)} moveEventsIntoNewGroup={this.moveEventsIntoNewGroup} - onOpenSceneVariables={this.editLayoutVariables} + onOpenSceneVariables={this.openSceneVariables} /> ); } @@ -856,7 +856,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component< }); }; - editLayoutVariables = (open: boolean = true) => { + openSceneVariables = (open: boolean = true) => { this.setState({ layoutVariablesDialogOpen: open }); }; @@ -2765,8 +2765,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component< this.editLayoutVariables(false)} - onApply={() => this.editLayoutVariables(false)} + onCancel={() => this.openSceneVariables(false)} + onApply={() => this.openSceneVariables(false)} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} isListLocked={false} /> diff --git a/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js index b5574a7660aa..9b2a4bda30f3 100644 --- a/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js +++ b/newIDE/app/src/LayersList/CompactLayerPropertiesEditor/index.js @@ -221,7 +221,6 @@ export const CompactLayerPropertiesEditor = ({ schema={layerPropertiesSchema} instances={[layer]} onInstancesModified={onLayersModified} - // $FlowFixMe[incompatible-type] onRefreshAllFields={forceRecomputeSchema} /> diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/ChildObjectPropertiesEditor.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/ChildObjectPropertiesEditor.js index c352fa741509..2da546db2b61 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/ChildObjectPropertiesEditor.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/ChildObjectPropertiesEditor.js @@ -89,7 +89,6 @@ export const ChildObjectPropertiesEditor = ({ onInstancesModified={() => { // TODO: undo/redo? }} - // $FlowFixMe[incompatible-type] onRefreshAllFields={forceRecomputeSchema} placeholder={This object has no properties.} customizeBasicSchema={schema => diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditor.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditor.js index 0d9153aaad6d..48979280b999 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditor.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditor.js @@ -124,8 +124,6 @@ export const CompactBehaviorPropertiesEditor = ({ onBehaviorUpdated, resourceManagementProps, }: CompactBehaviorPropertiesEditorProps): React.Node => { - const fullEditorLabel = behaviorMetadata.getOpenFullEditorLabel(); - const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute(); const propertiesSchema = React.useMemo( @@ -194,13 +192,12 @@ export const CompactBehaviorPropertiesEditor = ({ ? schema => getSchemaWithOpenFullEditorButton({ schema, - fullEditorLabel, + fullEditorLabel: behaviorMetadata.getOpenFullEditorLabel(), behavior, onOpenFullEditor, }) : null } - // $FlowFixMe[incompatible-type] onRefreshAllFields={forceRecomputeSchema} /> diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditorProps.flow.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditorProps.flow.js index 9f1cb660d71c..fa76328d1d55 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditorProps.flow.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/CompactBehaviorPropertiesEditorProps.flow.js @@ -8,7 +8,7 @@ export type CompactBehaviorPropertiesEditorProps = {| project: gdProject, behaviorMetadata: gdBehaviorMetadata, behavior: gdBehavior, - object: gdObject, + object: gdObject | null, behaviorOverriding: gdBehavior | null, initialInstance: gdInitialInstance | null, onOpenFullEditor?: () => void, diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js index febfb91efaad..fcdf9f4e3c6b 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js @@ -605,7 +605,6 @@ export const CompactObjectPropertiesEditor = ({ onEditObject, }) } - // $FlowFixMe[incompatible-type] onRefreshAllFields={forceRecomputeSchema} /> {shouldDisplayVariant && ( diff --git a/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js b/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js index c0aeefea4019..4be00077ce7d 100644 --- a/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js +++ b/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js @@ -52,7 +52,7 @@ export type SceneTreeViewItemProps = {| ...SceneTreeViewItemCommonProps, project: gdProject, onOpenLayoutProperties: (layout: ?gdLayout) => void, - onOpenLayoutVariables: (layout: ?gdLayout) => void, + openSceneVariables: (layout: ?gdLayout) => void, |}; export const getSceneTreeViewItemId = (scene: gdLayout): string => { @@ -153,7 +153,7 @@ export class SceneTreeViewItemContent implements TreeViewItemContent { { label: i18n._(t`Edit scene variables`), enabled: true, - click: () => this.props.onOpenLayoutVariables(this.scene), + click: () => this.props.openSceneVariables(this.scene), }, { label: i18n._(t`Set as start scene`), diff --git a/newIDE/app/src/ProjectManager/index.js b/newIDE/app/src/ProjectManager/index.js index 1d7a11ff5911..470bbb583357 100644 --- a/newIDE/app/src/ProjectManager/index.js +++ b/newIDE/app/src/ProjectManager/index.js @@ -600,7 +600,7 @@ const ProjectManager = React.forwardRef( const onOpenLayoutProperties = React.useCallback((layout: ?gdLayout) => { setEditedPropertiesLayout(layout); }, []); - const onOpenLayoutVariables = React.useCallback((layout: ?gdLayout) => { + const openSceneVariables = React.useCallback((layout: ?gdLayout) => { setEditedVariablesLayout(layout); }, []); @@ -906,7 +906,7 @@ const ProjectManager = React.forwardRef( onRenameLayout, onOpenLayout, onOpenLayoutProperties, - onOpenLayoutVariables, + openSceneVariables, } : null, [ @@ -924,7 +924,7 @@ const ProjectManager = React.forwardRef( onRenameLayout, onOpenLayout, onOpenLayoutProperties, - onOpenLayoutVariables, + openSceneVariables, ] ); @@ -1480,7 +1480,7 @@ const ProjectManager = React.forwardRef( }} onClose={() => onOpenLayoutProperties(null)} onEditVariables={() => { - onOpenLayoutVariables(editedPropertiesLayout); + openSceneVariables(editedPropertiesLayout); onOpenLayoutProperties(null); }} resourceManagementProps={resourceManagementProps} @@ -1497,10 +1497,10 @@ const ProjectManager = React.forwardRef( open project={project} layout={editedVariablesLayout} - onCancel={() => onOpenLayoutVariables(null)} + onCancel={() => openSceneVariables(null)} onApply={() => { triggerUnsavedChanges(); - onOpenLayoutVariables(null); + openSceneVariables(null); }} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} isListLocked={false} diff --git a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js index 493dd72f1911..02058c91dde3 100644 --- a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js +++ b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js @@ -61,7 +61,6 @@ const QuickBehaviorPropertiesEditor = ({ instances={[behavior]} onInstancesModified={onBehaviorUpdated} resourceManagementProps={resourceManagementProps} - // $FlowFixMe[incompatible-type] onRefreshAllFields={forceRecomputeSchema} /> diff --git a/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/CompactBehaviorSharedDataPropertiesEditor.js b/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/CompactBehaviorSharedDataPropertiesEditor.js new file mode 100644 index 000000000000..2710b9310ad1 --- /dev/null +++ b/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/CompactBehaviorSharedDataPropertiesEditor.js @@ -0,0 +1,63 @@ +// @flow +import * as React from 'react'; +import { ColumnStackLayout } from '../../UI/Layout'; +import { Trans } from '@lingui/macro'; +import { CompactPropertiesEditorByVisibility } from '../../CompactPropertiesEditor/CompactPropertiesEditorByVisibility'; +import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema'; +import { useForceRecompute } from '../../Utils/UseForceUpdate'; +import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource'; + +type CompactBehaviorPropertiesEditorProps = {| + project: gdProject, + behaviorMetadata: gdBehaviorMetadata, + behaviorSharedData: gdBehaviorsSharedData, + resourceManagementProps: ResourceManagementProps, + isAdvancedSectionInitiallyUncollapsed?: boolean, +|}; + +export const CompactBehaviorSharedDataPropertiesEditor = ({ + project, + behaviorMetadata, + behaviorSharedData, + resourceManagementProps, +}: CompactBehaviorPropertiesEditorProps): React.Node => { + const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute(); + + const propertiesSchema = React.useMemo( + () => { + if (schemaRecomputeTrigger) { + // schemaRecomputeTrigger allows to invalidate the schema when required. + } + const behaviorMetadataSharedProperties = behaviorMetadata.getSharedProperties(); + return propertiesMapToSchema({ + properties: behaviorMetadataSharedProperties, + defaultValueProperties: behaviorMetadataSharedProperties, + getPropertyValue: (instance, name) => + instance + .getProperties() + .get(name) + .getValue(), + onUpdateProperty: (instance, name, value) => { + instance.updateProperty(name, value); + }, + object: null, + visibility: 'All', + }); + }, + [schemaRecomputeTrigger, behaviorMetadata] + ); + + return ( + + Nothing to configure for this behavior.} + onRefreshAllFields={forceRecomputeSchema} + /> + + ); +}; diff --git a/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/CompactScenePropertiesSchema.js b/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/CompactScenePropertiesSchema.js new file mode 100644 index 000000000000..52554da8d90d --- /dev/null +++ b/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/CompactScenePropertiesSchema.js @@ -0,0 +1,163 @@ +// @flow + +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; +import { + type Schema, + type Field, +} from '../../PropertiesEditor/PropertiesEditorSchema'; +import { + rgbColorToRGBString, + rgbStringAndAlphaToRGBColor, + clampRgbComponent, +} from '../../Utils/ColorTransformer'; + +const getBackgroundColorField = ({ + i18n, + onBackgroundColorChanged, +}: {| + i18n: I18nType, + onBackgroundColorChanged: () => void, +|}): Field => ({ + name: 'BackgroundColor', + getLabel: () => i18n._(t`Background color`), + valueType: 'color', + getValue: (scene: gdLayout) => + rgbColorToRGBString({ + r: scene.getBackgroundColorRed(), + b: scene.getBackgroundColorBlue(), + g: scene.getBackgroundColorGreen(), + }), + setValue: (scene: gdLayout, newValue: string) => { + const backgroundColor = rgbStringAndAlphaToRGBColor(newValue); + scene.setBackgroundColor( + backgroundColor ? clampRgbComponent(backgroundColor.r) : 0, + backgroundColor ? clampRgbComponent(backgroundColor.g) : 0, + backgroundColor ? clampRgbComponent(backgroundColor.b) : 0 + ); + onBackgroundColorChanged(); + }, + visibility: 'basic', +}); + +const getShouldStopSoundsOnStartupField = ({ + i18n, +}: {| + i18n: I18nType, +|}): Field => ({ + name: 'ShouldStopSoundsOnStartup', + getLabel: () => i18n._(t`Stop music and sounds at scene startup`), + valueType: 'boolean', + getValue: (scene: gdLayout) => scene.stopSoundsOnStartup(), + setValue: (scene: gdLayout, newValue: boolean) => + scene.setStopSoundsOnStartup(newValue), + visibility: 'advanced', +}); + +const getResourcesPreloadingField = ({ + i18n, +}: {| + i18n: I18nType, +|}): Field => ({ + name: 'ResourcesPreloading', + getLabel: () => i18n._(t`Resources preloading`), + valueType: 'string', + getChoices: () => [ + { + value: 'inherit', + label: i18n._(t`Use the project setting`), + }, + { + value: 'at-startup', + label: i18n._(t`Always preload at startup`), + }, + { + value: 'never', + label: i18n._(t`Never preload`), + }, + ], + getValue: (scene: gdLayout) => scene.getResourcesPreloading(), + setValue: (scene: gdLayout, newValue: string) => + scene.setResourcesPreloading(newValue), + visibility: 'advanced', +}); + +const getResourcesUnloadingField = ({ i18n }: {| i18n: I18nType |}): Field => ({ + name: 'ResourcesUnloading', + getLabel: () => i18n._(t`Resources unloading`), + valueType: 'string', + getChoices: () => [ + { + value: 'inherit', + label: i18n._(t`Use the project setting`), + }, + { + value: 'at-scene-exit', + label: i18n._(t`Unload at scene exit`), + }, + { + value: 'never', + label: i18n._(t`Never unload`), + }, + ], + getValue: (scene: gdLayout) => scene.getResourcesUnloading(), + setValue: (scene: gdLayout, newValue: string) => + scene.setResourcesUnloading(newValue), + visibility: 'advanced', +}); + +const getWindowTitleField = ({ i18n }: {| i18n: I18nType |}): Field => ({ + name: 'WindowTitle', + getLabel: () => i18n._(t`Window title`), + valueType: 'string', + getValue: (scene: gdLayout) => scene.getWindowDefaultTitle(), + setValue: (scene: gdLayout, newValue: string) => + scene.setWindowDefaultTitle(newValue), + visibility: 'advanced', +}); + +export const makeSchema = ({ + i18n, + onBackgroundColorChanged, +}: {| + i18n: I18nType, + onBackgroundColorChanged: () => void, +|}): Schema => { + return [ + { + name: 'BackgroundColor', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getBackgroundColorField({ i18n, onBackgroundColorChanged })], + }, + { + name: 'ShouldStopSoundsOnStartup', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getShouldStopSoundsOnStartupField({ i18n })], + }, + { + name: 'ResourcesPreloading', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getResourcesPreloadingField({ i18n })], + }, + { + name: 'ResourcesUnloading', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getResourcesUnloadingField({ i18n })], + }, + { + name: 'WindowTitle', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getWindowTitleField({ i18n })], + }, + ]; +}; diff --git a/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/index.js b/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/index.js new file mode 100644 index 000000000000..fef36eed3b4b --- /dev/null +++ b/newIDE/app/src/SceneEditor/CompactScenePropertiesEditor/index.js @@ -0,0 +1,306 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import * as React from 'react'; +import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; +import VariablesList, { + type HistoryHandler, + type VariablesListInterface, +} from '../../VariablesList/VariablesList'; +import { type ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; +import ErrorBoundary from '../../UI/ErrorBoundary'; +import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; +import { Column, LargeSpacer, Line, marginsSize } from '../../UI/Grid'; +import Text from '../../UI/Text'; +import { Trans } from '@lingui/macro'; +import IconButton from '../../UI/IconButton'; +import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder'; +import { CompactBehaviorSharedDataPropertiesEditor } from './CompactBehaviorSharedDataPropertiesEditor'; +import { + TopLevelCollapsibleSection, + CollapsibleSubPanel, +} from '../../ObjectEditor/CompactObjectPropertiesEditor'; +import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource'; +import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout'; +import { IconContainer } from '../../UI/IconContainer'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import SceneIcon from '../../UI/CustomSvgIcons/Scene'; +import { usePersistedScrollPosition } from '../../Utils/UsePersistedScrollPosition'; +import Help from '../../UI/CustomSvgIcons/Help'; +import { getHelpLink } from '../../Utils/HelpLink'; +import Window from '../../Utils/Window'; +import Link from '../../UI/Link'; +import { CompactPropertiesEditorByVisibility } from '../../CompactPropertiesEditor/CompactPropertiesEditorByVisibility'; +import { useForceRecompute } from '../../Utils/UseForceUpdate'; +import { makeSchema } from './CompactScenePropertiesSchema'; +import EmptyMessage from '../../UI/EmptyMessage'; + +const gd: libGDevelop = global.gd; + +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 sceneVariablesHelpLink = getHelpLink( + '/all-features/variables/scene-variables' +); + +type Props = {| + project: gdProject, + scene: gdLayout, + resourceManagementProps: ResourceManagementProps, + openSceneVariables: () => void, + onBackgroundColorChanged: () => void, + projectScopedContainersAccessor: ProjectScopedContainersAccessor, + unsavedChanges?: ?UnsavedChanges, + i18n: I18nType, + historyHandler?: HistoryHandler, +|}; + +export const CompactScenePropertiesEditor = ({ + project, + resourceManagementProps, + scene, + openSceneVariables, + onBackgroundColorChanged, + projectScopedContainersAccessor, + unsavedChanges, + i18n, + historyHandler, +}: Props): React.Node => { + const forceUpdate = useForceUpdate(); + const [isPropertiesFolded, setIsPropertiesFolded] = React.useState(false); + const [isBehaviorsFolded, setIsBehaviorsFolded] = React.useState(false); + const [isVariablesFolded, setIsVariablesFolded] = React.useState(false); + const variablesListRef = React.useRef(null); + + const allVisibleBehaviors = scene + .getAllBehaviorSharedDataNames() + .toJSArray() + .map(behaviorName => scene.getBehaviorSharedData(behaviorName)) + .filter( + behaviorSharedData => + behaviorSharedData + .getProperties() + .keys() + .size() > 0 + ); + + const helpLink = getHelpLink('/interface/scene-editor/'); + + const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute(); + const scrollViewRef = React.useRef(null); + const scrollKey = 'scene-' + scene.ptr; + + const persistedScrollId = scene.getName(); + + const onScroll = usePersistedScrollPosition({ + project, + scrollViewRef, + scrollKey, + persistedScrollId, + persistedScrollType: 'scene', + }); + + const propertiesSchema = React.useMemo( + () => { + if (schemaRecomputeTrigger) { + // schemaRecomputeTrigger allows to invalidate the schema when required. + } + return makeSchema({ + i18n, + onBackgroundColorChanged, + }); + }, + [schemaRecomputeTrigger, i18n, onBackgroundColorChanged] + ); + + return ( + Scene properties} + scope="scene-editor-scene-properties" + > + + + + + + + + {scene.getName()} + + {helpLink && ( + { + Window.openExternalURL(helpLink); + }} + > + + + )} + + + + Properties} + isFolded={isPropertiesFolded} + toggleFolded={() => setIsPropertiesFolded(!isPropertiesFolded)} + renderContent={() => ( + + { + // TODO: undo/redo? + }} + resourceManagementProps={resourceManagementProps} + placeholder="" + onRefreshAllFields={forceRecomputeSchema} + /> + + )} + /> + {allVisibleBehaviors.length > 0 && ( + Behaviors} + isFolded={isBehaviorsFolded} + toggleFolded={() => setIsBehaviorsFolded(!isBehaviorsFolded)} + renderContent={() => ( + + {allVisibleBehaviors.map(behaviorSharedData => { + const behaviorTypeName = behaviorSharedData.getTypeName(); + const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata( + gd.JsPlatform.get(), + behaviorTypeName + ); + const iconUrl = behaviorMetadata.getIconFilename(); + return ( + ( + + )} + isFolded={behaviorSharedData.isFolded()} + toggleFolded={() => { + behaviorSharedData.setFolded( + !behaviorSharedData.isFolded() + ); + forceUpdate(); + }} + titleIcon={ + iconUrl ? ( + + ) : null + } + title={behaviorSharedData.getName()} + /> + ); + })} + + )} + /> + )} + Scene Variables} + isFolded={isVariablesFolded} + toggleFolded={() => setIsVariablesFolded(!isVariablesFolded)} + onOpenFullEditor={() => openSceneVariables()} + onAdd={() => { + if (variablesListRef.current) { + variablesListRef.current.addVariable(); + } + setIsVariablesFolded(false); + }} + renderContentAsHiddenWhenFolded={ + true /* Allows to keep a ref to the variables list for add button to work. */ + } + noContentMargin + renderContent={() => ( + + EventsRootVariablesFinder.findAllLayoutVariables( + project.getCurrentPlatform(), + project, + scene + ) + } + historyHandler={historyHandler} + toolbarIconStyle={styles.icon} + compactEmptyPlaceholderText={ + + There are no{' '} + + Window.openExternalURL(sceneVariablesHelpLink) + } + > + variables + {' '} + on this scene. + + } + isListLocked={false} + /> + )} + /> + + + + + Click on an instance on the canvas or an object in the list to + display their properties. + + + + + + + ); +}; diff --git a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js index b84a0ba369db..e5736032cfe4 100644 --- a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js +++ b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js @@ -72,6 +72,7 @@ export type SceneEditorsDisplayProps = {| onLayersModified: () => void, onLayersVisibilityInEditorChanged: () => void, onBackgroundColorChanged: () => void, + openSceneVariables: () => void, onObjectCreated: ( objects: Array, isTheFirstOfItsTypeInProject: boolean diff --git a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js index 234d8500f4d2..e7e85e1ee6c1 100644 --- a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js +++ b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js @@ -2,7 +2,6 @@ import * as React from 'react'; import { type I18n as I18nType } from '@lingui/core'; import Paper from '../UI/Paper'; -import EmptyMessage from '../UI/EmptyMessage'; import useForceUpdate from '../Utils/UseForceUpdate'; import { CompactInstancePropertiesEditor } from '../InstancesEditor/CompactInstancePropertiesEditor'; import { Trans } from '@lingui/macro'; @@ -15,6 +14,7 @@ import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import { CompactLayerPropertiesEditor } from '../LayersList/CompactLayerPropertiesEditor'; import { CompactEventsBasedObjectVariantPropertiesEditor } from '../SceneEditor/CompactEventsBasedObjectVariantPropertiesEditor'; +import { CompactScenePropertiesEditor } from './CompactScenePropertiesEditor'; import Rectangle from '../Utils/Rectangle'; export const styles = { @@ -83,6 +83,10 @@ type Props = {| onEventsBasedObjectChildrenEdited: ( eventsBasedObject: gdEventsBasedObject ) => void, + + // For scenes + onBackgroundColorChanged: () => void, + openSceneVariables: () => void, |}; export type InstanceOrObjectPropertiesEditorInterface = {| @@ -96,16 +100,22 @@ export const InstanceOrObjectPropertiesEditorContainer: React.ComponentType<{ }> = React.forwardRef( (props, ref) => { const forceUpdate = useForceUpdate(); - // $FlowFixMe[incompatible-type] - React.useImperativeHandle(ref, () => ({ - forceUpdate, - getEditorTitle: () => - lastSelectionType === 'instance' ? ( - Instance properties - ) : ( - Object properties - ), - })); + React.useImperativeHandle( + ref, + () => ({ + forceUpdate, + getEditorTitle: () => + lastSelectionType === 'instance' ? ( + Instance properties + ) : lastSelectionType === 'object' ? ( + Object properties + ) : lastSelectionType === 'layer' ? ( + Layer properties + ) : ( + Scene properties + ), + }) + ); const { project, @@ -156,6 +166,10 @@ export const InstanceOrObjectPropertiesEditorContainer: React.ComponentType<{ layout, objectsContainer, globalObjectsContainer, + + // For scenes + onBackgroundColorChanged, + openSceneVariables, } = props; return ( @@ -233,14 +247,18 @@ export const InstanceOrObjectPropertiesEditorContainer: React.ComponentType<{ unsavedChanges={unsavedChanges} i18n={i18n} /> - ) : ( - - - Click on an instance on the canvas or an object in the list to - display their properties. - - - )} + ) : layout ? ( + + ) : null} ); } diff --git a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js index b0b5f5f15a4a..02f2a2eeb689 100644 --- a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js @@ -335,6 +335,8 @@ const MosaicEditorsDisplay: React.ComponentType<{ onEventsBasedObjectChildrenEdited={ props.onEventsBasedObjectChildrenEdited } + onBackgroundColorChanged={props.onBackgroundColorChanged} + openSceneVariables={props.openSceneVariables} /> )} diff --git a/newIDE/app/src/SceneEditor/ScenePropertiesDialog.js b/newIDE/app/src/SceneEditor/ScenePropertiesDialog.js index 6b73bedf0e10..b54a8eb941d5 100644 --- a/newIDE/app/src/SceneEditor/ScenePropertiesDialog.js +++ b/newIDE/app/src/SceneEditor/ScenePropertiesDialog.js @@ -16,6 +16,7 @@ import { ColumnStackLayout } from '../UI/Layout'; import { rgbColorToRGBString, rgbStringAndAlphaToRGBColor, + clampRgbComponent, type RGBColor, } from '../Utils/ColorTransformer'; import HelpIcon from '../UI/HelpIcon'; @@ -109,9 +110,9 @@ const ScenePropertiesDialog = ({ layout.getBackgroundColorGreen() !== backgroundColor.g && layout.getBackgroundColorBlue() !== backgroundColor.b; layout.setBackgroundColor( - backgroundColor ? backgroundColor.r : 0, - backgroundColor ? backgroundColor.g : 0, - backgroundColor ? backgroundColor.b : 0 + backgroundColor ? clampRgbComponent(backgroundColor.r) : 0, + backgroundColor ? clampRgbComponent(backgroundColor.g) : 0, + backgroundColor ? clampRgbComponent(backgroundColor.b) : 0 ); onApply(); if (hasBackgroundColorChanged) { @@ -287,9 +288,7 @@ const ScenePropertiesDialog = ({ /> Stop music and sounds at the beginning of this scene - } + label={Stop music and sounds at scene startup} onCheck={(e, check) => setShouldStopSoundsOnStartup(check)} /> {!some(propertiesEditors) && ( diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js index f55d89520d65..7793d4b85ef3 100644 --- a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js @@ -502,6 +502,10 @@ const SwipeableDrawerEditorsDisplay: React.ComponentType<{ onEventsBasedObjectChildrenEdited={ props.onEventsBasedObjectChildrenEdited } + onBackgroundColorChanged={ + props.onBackgroundColorChanged + } + openSceneVariables={props.openSceneVariables} /> )} diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js index 881968b94139..3e066d472b27 100644 --- a/newIDE/app/src/SceneEditor/index.js +++ b/newIDE/app/src/SceneEditor/index.js @@ -705,7 +705,7 @@ export default class SceneEditor extends React.Component { redo={this.redo} onOpenSettings={this.openSceneProperties} settingsIcon={editSceneIconReactNode} - onOpenSceneVariables={this.editLayoutVariables} + onOpenSceneVariables={this.openSceneVariables} /> ); } else { @@ -735,7 +735,7 @@ export default class SceneEditor extends React.Component { redo={this.redo} onOpenSettings={this.openSceneProperties} settingsIcon={editSceneIconReactNode} - onOpenSceneVariables={this.editLayoutVariables} + onOpenSceneVariables={this.openSceneVariables} /> ); } @@ -859,7 +859,7 @@ export default class SceneEditor extends React.Component { this.setState({ variablesEditedInstance: instance }); }; - editLayoutVariables = (open: boolean = true) => { + openSceneVariables = (open: boolean = true) => { this.setState({ layoutVariablesDialogOpen: open }); }; @@ -1608,6 +1608,8 @@ export default class SceneEditor extends React.Component { }; _sendSetBackgroundColor = () => { + this.forceUpdatePropertiesEditor(); + this.forceUpdateLayersList(); const { previewDebuggerServer, layout } = this.props; if (!layout) { return; @@ -2951,6 +2953,7 @@ export default class SceneEditor extends React.Component { onEventsBasedObjectChildrenEdited={ this.props.onEventsBasedObjectChildrenEdited } + openSceneVariables={this.openSceneVariables} /> {editedObjectWithContext && ( @@ -3229,7 +3232,7 @@ export default class SceneEditor extends React.Component { layout={layout} onClose={() => this.openSceneProperties(false)} onApply={() => this.openSceneProperties(false)} - onEditVariables={() => this.editLayoutVariables(true)} + onEditVariables={() => this.openSceneVariables(true)} onOpenMoreSettings={this.props.onOpenMoreSettings} resourceManagementProps={ this.props.resourceManagementProps @@ -3286,8 +3289,8 @@ export default class SceneEditor extends React.Component { open project={project} layout={layout} - onApply={() => this.editLayoutVariables(false)} - onCancel={() => this.editLayoutVariables(false)} + onApply={() => this.openSceneVariables(false)} + onCancel={() => this.openSceneVariables(false)} hotReloadPreviewButtonProps={ this.props.hotReloadPreviewButtonProps } diff --git a/newIDE/app/src/UI/CompactColorField/index.js b/newIDE/app/src/UI/CompactColorField/index.js index 59bab0815e08..51fee0583f59 100644 --- a/newIDE/app/src/UI/CompactColorField/index.js +++ b/newIDE/app/src/UI/CompactColorField/index.js @@ -32,13 +32,31 @@ export const CompactColorField = ({ placeholder, }: CompactColorFieldProps): React.MixedElement => { const idToUse = React.useRef(id || makeTimestampedId()); + const [colorValue, setColorValue] = React.useState(color); - // alpha can be equal to 0, so we have to check if it is not undefined const [alphaValue, setAlphaValue] = React.useState( + // alpha can be equal to 0, so we have to check if it is not undefined // $FlowFixMe[constant-condition] !disableAlpha && alpha !== undefined ? alpha : 1 ); + React.useEffect( + () => { + setColorValue(color); + }, + [color] + ); + + React.useEffect( + () => { + // $FlowFixMe[constant-condition] + if (!disableAlpha && alpha !== undefined) { + setAlphaValue(alpha); + } + }, + [alpha, disableAlpha] + ); + const handleChange = (newColor: string, newAlpha: number) => { setColorValue(newColor); setAlphaValue(newAlpha); @@ -58,7 +76,7 @@ export const CompactColorField = ({ // $FlowFixMe[constant-condition] const newAlpha = disableAlpha ? 1 : color.rgb.a; setColorValue(rgbString); - if (newAlpha) setAlphaValue(newAlpha); + if (newAlpha !== undefined) setAlphaValue(newAlpha); onChange(rgbString, newAlpha); }; diff --git a/newIDE/app/src/UI/ErrorBoundary.js b/newIDE/app/src/UI/ErrorBoundary.js index 6afc6c0aea19..31134a98f8b1 100644 --- a/newIDE/app/src/UI/ErrorBoundary.js +++ b/newIDE/app/src/UI/ErrorBoundary.js @@ -44,6 +44,7 @@ type ErrorBoundaryScope = | 'preferences' | 'profile' | 'scene-editor' + | 'scene-editor-scene-properties' | 'scene-editor-instance-properties' | 'scene-editor-object-properties' | 'scene-editor-layer-properties' diff --git a/newIDE/app/src/Utils/ColorTransformer.js b/newIDE/app/src/Utils/ColorTransformer.js index 83caa05d76d1..d5a92e41ea63 100644 --- a/newIDE/app/src/Utils/ColorTransformer.js +++ b/newIDE/app/src/Utils/ColorTransformer.js @@ -234,3 +234,6 @@ export const rgbToHsl = (r: number, g: number, b: number): number[] => { export const isLightRgbColor = (rgbColor: RGBColor): boolean => { return rgbColor.r * 0.299 + rgbColor.g * 0.587 + rgbColor.b * 0.114 > 186; }; + +export const clampRgbComponent = (component: number): number => + Math.min(Math.max(0, component), 255); diff --git a/newIDE/app/src/Utils/UseForceUpdate.js b/newIDE/app/src/Utils/UseForceUpdate.js index aa3042515a95..1f62ad294a63 100644 --- a/newIDE/app/src/Utils/UseForceUpdate.js +++ b/newIDE/app/src/Utils/UseForceUpdate.js @@ -10,7 +10,7 @@ export default function useForceUpdate(): () => void { return forceUpdate; } -export function useForceRecompute(): Array<{} | (() => void)> { +export function useForceRecompute(): [{}, () => void] { const [recomputeTrigger, updateState] = React.useState({}); const forceRecompute = React.useCallback(() => updateState({}), []); diff --git a/newIDE/app/src/Utils/UsePersistedScrollPosition.js b/newIDE/app/src/Utils/UsePersistedScrollPosition.js index daabe6994234..016a0f8b772c 100644 --- a/newIDE/app/src/Utils/UsePersistedScrollPosition.js +++ b/newIDE/app/src/Utils/UsePersistedScrollPosition.js @@ -7,7 +7,7 @@ type Props = {| project: gdProject, scrollViewRef: {| current: ?ScrollViewInterface |}, scrollKey: string, - persistedScrollType: 'instances-of-object' | 'object', + persistedScrollType: 'instances-of-object' | 'object' | 'scene', persistedScrollId: string | null, saveDebounceTimeInMs?: number, |}; diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactScenePropertiesEditor.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactScenePropertiesEditor.stories.js new file mode 100644 index 000000000000..b468cc2fedfa --- /dev/null +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactScenePropertiesEditor.stories.js @@ -0,0 +1,64 @@ +// @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 { CompactScenePropertiesEditor } from '../../../SceneEditor/CompactScenePropertiesEditor'; +import SerializedObjectDisplay from '../../SerializedObjectDisplay'; +import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider'; +import fakeResourceManagementProps from '../../FakeResourceManagement'; + +export default { + title: 'LayoutEditor/CompactScenePropertiesEditor', + component: CompactScenePropertiesEditor, + decorators: [paperDecorator], +}; + +export const Default = (): React.Node => ( + + + {({ i18n }) => ( + + + + )} + + +); + +export const Empty = (): React.Node => ( + + + {({ i18n }) => ( + + + + )} + + +);