diff --git a/newIDE/app/src/AiGeneration/AskAiEditorContainer.js b/newIDE/app/src/AiGeneration/AskAiEditorContainer.js index a0d9cfc3a714..e5f110905b50 100644 --- a/newIDE/app/src/AiGeneration/AskAiEditorContainer.js +++ b/newIDE/app/src/AiGeneration/AskAiEditorContainer.js @@ -4,13 +4,14 @@ import { type I18n as I18nType } from '@lingui/core'; import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow'; import { exceptionallyGuardAgainstDeadObject } from '../Utils/IsNullPtr'; import { I18n } from '@lingui/react'; +import { type RenderEditorContainerPropsWithRef } from '../MainFrame/EditorContainers/BaseEditor'; import { - type RenderEditorContainerPropsWithRef, type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from '../MainFrame/EditorContainers/BaseEditor'; + type SceneRenamedOutsideEditorChanges, +} from '../EditorFunctions/OutsideEditorChanges'; import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects'; import Paper from '../UI/Paper'; import { AiRequestChat, type AiRequestChatInterface } from './AiRequestChat'; @@ -152,6 +153,9 @@ type Props = {| onObjectGroupsModifiedOutsideEditor: ( changes: ObjectGroupsOutsideEditorChanges ) => void, + onSceneRenamedOutsideEditor: ( + changes: SceneRenamedOutsideEditorChanges + ) => void, onWillInstallExtension: (extensionNames: Array) => void, onExtensionInstalled: (extensionNames: Array) => void, onOpenAskAi: ({| @@ -250,6 +254,7 @@ export const AskAiEditor: React.ComponentType = React.memo( onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, onWillInstallExtension, onExtensionInstalled, onOpenAskAi, @@ -916,6 +921,7 @@ export const AskAiEditor: React.ComponentType = React.memo( onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, i18n, onWillInstallExtension, onExtensionInstalled, @@ -1627,6 +1633,7 @@ export const renderAskAiEditorContainer = ( onObjectGroupsModifiedOutsideEditor={ props.onObjectGroupsModifiedOutsideEditor } + onSceneRenamedOutsideEditor={props.onSceneRenamedOutsideEditor} onWillInstallExtension={props.onWillInstallExtension} onExtensionInstalled={props.onExtensionInstalled} onOpenAskAi={props.onOpenAskAi} diff --git a/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js b/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js index 73ffe86d8370..093e3d983040 100644 --- a/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js +++ b/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js @@ -589,6 +589,7 @@ export const AskAiStandAloneForm = ({ onInstancesModifiedOutsideEditor: () => {}, onObjectsModifiedOutsideEditor: () => {}, onObjectGroupsModifiedOutsideEditor: () => {}, + onSceneRenamedOutsideEditor: () => {}, onWillInstallExtension, onExtensionInstalled, isReadyToProcessFunctionCalls: true, diff --git a/newIDE/app/src/AiGeneration/Utils.js b/newIDE/app/src/AiGeneration/Utils.js index fff59f7e2f23..1d01b6e013ac 100644 --- a/newIDE/app/src/AiGeneration/Utils.js +++ b/newIDE/app/src/AiGeneration/Utils.js @@ -6,7 +6,8 @@ import { type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from '../MainFrame/EditorContainers/BaseEditor'; + type SceneRenamedOutsideEditorChanges, +} from '../EditorFunctions/OutsideEditorChanges'; import { getAiRequest, getAiRequestSuggestions, @@ -222,6 +223,7 @@ export const useProcessFunctionCalls = ({ onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, onWillInstallExtension, onExtensionInstalled, isReadyToProcessFunctionCalls, @@ -259,6 +261,9 @@ export const useProcessFunctionCalls = ({ onObjectGroupsModifiedOutsideEditor: ( changes: ObjectGroupsOutsideEditorChanges ) => void, + onSceneRenamedOutsideEditor: ( + changes: SceneRenamedOutsideEditorChanges + ) => void, onWillInstallExtension: (extensionNames: Array) => void, onExtensionInstalled: (extensionNames: Array) => void, isReadyToProcessFunctionCalls: boolean, @@ -483,6 +488,7 @@ export const useProcessFunctionCalls = ({ const accumulatedInstancesScenes: Set = new Set(); const accumulatedObjectsChanges: Map = new Map(); const accumulatedObjectGroupsScenes: Set = new Set(); + const accumulatedSceneRenames: Array = []; const flushAccumulatedOutsideEditorChanges = () => { accumulatedSceneEventsChanges.forEach((eventIds, scene) => onSceneEventsModifiedOutsideEditor({ @@ -499,6 +505,9 @@ export const useProcessFunctionCalls = ({ accumulatedObjectGroupsScenes.forEach(scene => onObjectGroupsModifiedOutsideEditor({ scene }) ); + accumulatedSceneRenames.forEach(changes => + onSceneRenamedOutsideEditor(changes) + ); }; try { @@ -550,6 +559,9 @@ export const useProcessFunctionCalls = ({ onObjectGroupsModifiedOutsideEditor: changes => { accumulatedObjectGroupsScenes.add(changes.scene); }, + onSceneRenamedOutsideEditor: changes => { + accumulatedSceneRenames.push(changes); + }, ensureExtensionInstalled, onWillInstallExtension, onExtensionInstalled, @@ -600,6 +612,7 @@ export const useProcessFunctionCalls = ({ onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, ensureExtensionInstalled, onWillInstallExtension, onExtensionInstalled, diff --git a/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js b/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js index a4330194831d..755fc988bc52 100644 --- a/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js +++ b/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js @@ -16,12 +16,15 @@ import { type RelatedAiRequestLastMessages, type ResourceSearchAndInstallOptions, type ResourceSearchAndInstallResult, + type ToolOptions, +} from '.'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, - type ToolOptions, -} from '.'; + type SceneRenamedOutsideEditorChanges, +} from './OutsideEditorChanges'; import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader'; import { type EnsureExtensionInstalledOptions } from '../AiGeneration/UseEnsureExtensionInstalled'; @@ -48,6 +51,9 @@ type ProcessEditorFunctionCallsOptions = {| onObjectGroupsModifiedOutsideEditor: ( changes: ObjectGroupsOutsideEditorChanges ) => void, + onSceneRenamedOutsideEditor: ( + changes: SceneRenamedOutsideEditorChanges + ) => void, ensureExtensionInstalled: ( options: EnsureExtensionInstalledOptions ) => Promise, @@ -73,6 +79,7 @@ export const processEditorFunctionCalls = async ({ onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, relatedAiRequestId, getRelatedAiRequestLastMessages, ensureExtensionInstalled, @@ -187,6 +194,7 @@ export const processEditorFunctionCalls = async ({ onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, ensureExtensionInstalled, onWillInstallExtension, onExtensionInstalled, diff --git a/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js b/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js index 57a1626c5223..a4160d1e4980 100644 --- a/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js +++ b/newIDE/app/src/EditorFunctions/EditorFunctions.spec.js @@ -37,6 +37,7 @@ describe('editorFunctions', () => { onInstancesModifiedOutsideEditor: jest.fn(), onObjectGroupsModifiedOutsideEditor: jest.fn(), onSceneEventsModifiedOutsideEditor: jest.fn(), + onSceneRenamedOutsideEditor: jest.fn(), toolOptions: { includeEventsJson: true, }, @@ -2468,5 +2469,65 @@ describe('editorFunctions', () => { expect(project.getScaleMode()).toBe('nearest'); expect(project.getName()).toBe('My Game'); }); + + it('renames the scene when setting the "name" property', async () => { + const onSceneRenamedOutsideEditor: JestMockFn = jest.fn(); + const wasFirstScene = project.getFirstLayout() === 'TestScene'; + + const result = await editorFunctions.change_scene_properties_layers_effects_groups.launchFunction( + { + ...makeFakeLaunchFunctionOptionsWithProject(project), + onSceneRenamedOutsideEditor, + args: { + scene_name: 'TestScene', + changed_properties: [ + { property_name: 'name', new_value: 'GameScene' }, + ], + }, + } + ); + + expect(result.success).toBe(true); + expect(result.message).toContain( + 'Renamed scene "TestScene" to "GameScene"' + ); + + // The scene is actually renamed in the project. + expect(project.hasLayoutNamed('TestScene')).toBe(false); + expect(project.hasLayoutNamed('GameScene')).toBe(true); + // The kept layout pointer is the same one. + expect(testScene.getName()).toBe('GameScene'); + if (wasFirstScene) { + expect(project.getFirstLayout()).toBe('GameScene'); + } + + // The editor is notified so open tabs can be kept and updated. + expect(onSceneRenamedOutsideEditor).toHaveBeenCalledWith({ + oldName: 'TestScene', + newName: 'GameScene', + }); + }); + + it('does nothing when renaming a scene to its current name', async () => { + const onSceneRenamedOutsideEditor: JestMockFn = jest.fn(); + + const result = await editorFunctions.change_scene_properties_layers_effects_groups.launchFunction( + { + ...makeFakeLaunchFunctionOptionsWithProject(project), + onSceneRenamedOutsideEditor, + args: { + scene_name: 'TestScene', + changed_properties: [ + { property_name: 'name', new_value: 'TestScene' }, + ], + }, + } + ); + + expect(result.success).toBe(true); + expect(result.message).toContain('Scene already named "TestScene".'); + expect(project.hasLayoutNamed('TestScene')).toBe(true); + expect(onSceneRenamedOutsideEditor).not.toHaveBeenCalled(); + }); }); }); diff --git a/newIDE/app/src/EditorFunctions/OutsideEditorChanges.js b/newIDE/app/src/EditorFunctions/OutsideEditorChanges.js new file mode 100644 index 000000000000..8486b2ea7308 --- /dev/null +++ b/newIDE/app/src/EditorFunctions/OutsideEditorChanges.js @@ -0,0 +1,24 @@ +// @flow + +export type SceneEventsOutsideEditorChanges = {| + scene: gdLayout, + newOrChangedAiGeneratedEventIds: Set, +|}; + +export type InstancesOutsideEditorChanges = {| + scene: gdLayout, +|}; + +export type ObjectsOutsideEditorChanges = {| + scene: gdLayout, + isNewObjectTypeUsed: boolean, +|}; + +export type ObjectGroupsOutsideEditorChanges = {| + scene: gdLayout, +|}; + +export type SceneRenamedOutsideEditorChanges = {| + oldName: string, + newName: string, +|}; diff --git a/newIDE/app/src/EditorFunctions/index.js b/newIDE/app/src/EditorFunctions/index.js index 27d1c007f265..b506374fbcee 100644 --- a/newIDE/app/src/EditorFunctions/index.js +++ b/newIDE/app/src/EditorFunctions/index.js @@ -1,6 +1,9 @@ // @flow import * as React from 'react'; -import { getInstancesInLayoutForLayer } from '../Utils/Layout'; +import { + getInstancesInLayoutForLayer, + renameLayoutInProject, +} from '../Utils/Layout'; import { mapFor, mapVector } from '../Utils/MapFor'; import { SafeExtractor } from '../Utils/SafeExtractor'; import { @@ -35,6 +38,13 @@ import { } from '../ProjectCreation/CreateProject'; import { retryIfFailed } from '../Utils/RetryIfFailed'; import newNameGenerator from '../Utils/NewNameGenerator'; +import type { + SceneEventsOutsideEditorChanges, + InstancesOutsideEditorChanges, + ObjectsOutsideEditorChanges, + ObjectGroupsOutsideEditorChanges, + SceneRenamedOutsideEditorChanges, +} from './OutsideEditorChanges'; import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset'; import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example'; import { swapAsset } from '../AssetStore/AssetSwapper'; @@ -224,24 +234,6 @@ export type EditorCallbacks = {| |}>, |}; -export type SceneEventsOutsideEditorChanges = {| - scene: gdLayout, - newOrChangedAiGeneratedEventIds: Set, -|}; - -export type InstancesOutsideEditorChanges = {| - scene: gdLayout, -|}; - -export type ObjectsOutsideEditorChanges = {| - scene: gdLayout, - isNewObjectTypeUsed: boolean, -|}; - -export type ObjectGroupsOutsideEditorChanges = {| - scene: gdLayout, -|}; - export type ToolOptions = { includeEventsJson?: boolean, ... @@ -286,6 +278,9 @@ type LaunchFunctionOptionsWithoutProject = {| onObjectGroupsModifiedOutsideEditor: ( changes: ObjectGroupsOutsideEditorChanges ) => void, + onSceneRenamedOutsideEditor: ( + changes: SceneRenamedOutsideEditorChanges + ) => void, ensureExtensionInstalled: ( options: EnsureExtensionInstalledOptions ) => Promise, @@ -4775,6 +4770,7 @@ const inspectScenePropertiesLayersEffects: EditorFunction = { success: true, propertiesLayersEffectsForSceneNamed: scene.getName(), properties: { + name: scene.getName(), backgroundColor: rgbColorToHex( scene.getBackgroundColorRed(), scene.getBackgroundColorGreen(), @@ -4930,6 +4926,7 @@ const changeScenePropertiesLayersEffectsGroups: EditorFunction = { args, onInstancesModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, }) => { const scene_name = extractRequiredString(args, 'scene_name'); @@ -4977,7 +4974,25 @@ const changeScenePropertiesLayersEffectsGroups: EditorFunction = { return; } - if (isFuzzyMatch(propertyName, 'backgroundColor')) { + if (isFuzzyMatch(propertyName, 'name')) { + const oldName = scene.getName(); + if (newValue === oldName) { + changes.push(`Scene already named "${newValue}".`); + return; + } + + const newSceneName = newNameGenerator( + gd.Project.getSafeName(newValue), + tentativeNewName => project.hasLayoutNamed(tentativeNewName) + ); + + renameLayoutInProject(project, oldName, newSceneName); + onSceneRenamedOutsideEditor({ oldName, newName: newSceneName }); + + changes.push( + `Renamed scene "${oldName}" to "${newSceneName}" (events and references updated).` + ); + } else if (isFuzzyMatch(propertyName, 'backgroundColor')) { const colorAsRgb = hexNumberToRGBArray(rgbOrHexToHexNumber(newValue)); scene.setBackgroundColor(colorAsRgb[0], colorAsRgb[1], colorAsRgb[2]); changes.push( diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js index 162490b05837..32205805c3c1 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js +++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js @@ -24,6 +24,13 @@ import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects'; import { type CreateProjectResult } from '../../Utils/UseCreateProject'; import { type OpenAskAiOptions } from '../../AiGeneration/Utils'; import type { NavigateToEventFromGlobalSearchParams } from '../../Utils/Search'; +import type { + SceneEventsOutsideEditorChanges, + InstancesOutsideEditorChanges, + ObjectsOutsideEditorChanges, + ObjectGroupsOutsideEditorChanges, + SceneRenamedOutsideEditorChanges, +} from '../../EditorFunctions/OutsideEditorChanges'; export type EditorContainerExtraProps = {| // Events function extension editor @@ -38,24 +45,6 @@ export type EditorContainerExtraProps = {| continueProcessingFunctionCallsOnMount?: boolean, |}; -export type SceneEventsOutsideEditorChanges = {| - scene: gdLayout, - newOrChangedAiGeneratedEventIds: Set, -|}; - -export type InstancesOutsideEditorChanges = {| - scene: gdLayout, -|}; - -export type ObjectsOutsideEditorChanges = {| - scene: gdLayout, - isNewObjectTypeUsed: boolean, -|}; - -export type ObjectGroupsOutsideEditorChanges = {| - scene: gdLayout, -|}; - export type RenderEditorContainerProps = {| isActive: boolean, gameEditorMode: 'embedded-game' | 'instances-editor', @@ -242,6 +231,9 @@ export type RenderEditorContainerProps = {| onObjectGroupsModifiedOutsideEditor: ( changes: ObjectGroupsOutsideEditorChanges ) => void, + onSceneRenamedOutsideEditor: ( + changes: SceneRenamedOutsideEditorChanges + ) => void, // Events editing onSceneEventsModifiedOutsideEditor: ( diff --git a/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js index 5adaf7508ff3..9125fc1671ca 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js @@ -3,11 +3,13 @@ import * as React from 'react'; import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings'; import { registerOnResourceExternallyChangedCallback, diff --git a/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js index fc276c75e3f3..b75c8821f125 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js @@ -6,11 +6,13 @@ import Debugger from '../../Debugger'; import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import SubscriptionChecker, { type SubscriptionCheckerInterface, } from '../../Profile/Subscription/SubscriptionChecker'; diff --git a/newIDE/app/src/MainFrame/EditorContainers/EventsEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/EventsEditorContainer.js index 450c6228c30a..2cd3f63915ef 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/EventsEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/EventsEditorContainer.js @@ -6,11 +6,13 @@ import { sendEventsExtractedAsFunction } from '../../Utils/Analytics/EventSender import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects'; import { diff --git a/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js index 2dc459107fc6..44787a8281ee 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js @@ -4,11 +4,13 @@ import EventsFunctionsExtensionEditor from '../../EventsFunctionsExtensionEditor import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects'; import { setEditorHotReloadNeeded, diff --git a/newIDE/app/src/MainFrame/EditorContainers/ExternalEventsEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/ExternalEventsEditorContainer.js index f55816da5870..657bcd5eca77 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/ExternalEventsEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/ExternalEventsEditorContainer.js @@ -7,11 +7,13 @@ import PlaceholderMessage from '../../UI/PlaceholderMessage'; import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import ExternalPropertiesDialog, { type ExternalProperties, } from './ExternalPropertiesDialog'; diff --git a/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js index c4d1ca0a3e32..733709efd32c 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js @@ -12,11 +12,13 @@ import PlaceholderMessage from '../../UI/PlaceholderMessage'; import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import ExternalPropertiesDialog, { type ExternalProperties, } from './ExternalPropertiesDialog'; diff --git a/newIDE/app/src/MainFrame/EditorContainers/GlobalEventsSearchEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/GlobalEventsSearchEditorContainer.js index 9c36860bf360..6a30a0e334ba 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/GlobalEventsSearchEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/GlobalEventsSearchEditorContainer.js @@ -3,11 +3,13 @@ import * as React from 'react'; import type { RenderEditorContainerProps, RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import type { SceneEventsOutsideEditorChanges, InstancesOutsideEditorChanges, ObjectsOutsideEditorChanges, ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects'; import { type HotReloadSteps } from '../../EmbeddedGame/EmbeddedGameFrame'; import { diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index 71778d2e1a7e..c3da07f2ea23 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -1,13 +1,13 @@ // @flow import * as React from 'react'; import { I18n } from '@lingui/react'; +import { type RenderEditorContainerPropsWithRef } from '../BaseEditor'; import { - type RenderEditorContainerPropsWithRef, type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from '../BaseEditor'; +} from '../../../EditorFunctions/OutsideEditorChanges'; import { type FileMetadataAndStorageProviderName, type FileMetadata, diff --git a/newIDE/app/src/MainFrame/EditorContainers/ResourcesEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/ResourcesEditorContainer.js index b3f5332215b3..b8e5ec0ad8d1 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/ResourcesEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/ResourcesEditorContainer.js @@ -3,11 +3,13 @@ import React from 'react'; import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import ResourcesEditor from '../../ResourcesEditor'; import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects'; import { diff --git a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js index 6ed109f553fd..c9d3a64cdb65 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js @@ -9,11 +9,13 @@ import { import { type RenderEditorContainerProps, type RenderEditorContainerPropsWithRef, +} from './BaseEditor'; +import { type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './BaseEditor'; +} from '../../EditorFunctions/OutsideEditorChanges'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects'; import { diff --git a/newIDE/app/src/MainFrame/EditorTabsPane.js b/newIDE/app/src/MainFrame/EditorTabsPane.js index 73f091106092..286e0f93a362 100644 --- a/newIDE/app/src/MainFrame/EditorTabsPane.js +++ b/newIDE/app/src/MainFrame/EditorTabsPane.js @@ -29,7 +29,8 @@ import { type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './EditorContainers/BaseEditor'; + type SceneRenamedOutsideEditorChanges, +} from '../EditorFunctions/OutsideEditorChanges'; import { type NavigateToEventFromGlobalSearchParams } from '../Utils/Search'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; @@ -284,6 +285,9 @@ export type EditorTabsPaneCommonProps = {| onObjectGroupsModifiedOutsideEditor: ( changes: ObjectGroupsOutsideEditorChanges ) => void, + onSceneRenamedOutsideEditor: ( + changes: SceneRenamedOutsideEditorChanges + ) => void, onWillInstallExtension: (extensionNames: Array) => void, onExtensionInstalled: (extensionNames: Array) => void, onLoadEventsFunctionsExtensions: ({| @@ -400,6 +404,7 @@ const EditorTabsPane: React.ComponentType<{ onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor, onWillInstallExtension, onExtensionInstalled, onEffectAdded, @@ -865,6 +870,7 @@ const EditorTabsPane: React.ComponentType<{ onInstancesModifiedOutsideEditor: onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor: onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor: onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor: onSceneRenamedOutsideEditor, onWillInstallExtension: onWillInstallExtension, onExtensionInstalled: onExtensionInstalled, onEffectAdded: onEffectAdded, diff --git a/newIDE/app/src/MainFrame/PoppedOutEditorContainerWindow.js b/newIDE/app/src/MainFrame/PoppedOutEditorContainerWindow.js index edb1897a1b03..afe24327fa3a 100644 --- a/newIDE/app/src/MainFrame/PoppedOutEditorContainerWindow.js +++ b/newIDE/app/src/MainFrame/PoppedOutEditorContainerWindow.js @@ -353,6 +353,8 @@ const PoppedOutEditorContainerWindow = (props: Props): React.Node => { props.onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor: props.onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor: + props.onSceneRenamedOutsideEditor, onWillInstallExtension: props.onWillInstallExtension, onExtensionInstalled: props.onExtensionInstalled, onEffectAdded: props.onEffectAdded, diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 1c84093c1d6a..050b79eee413 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -81,13 +81,14 @@ import { import { renderAskAiEditorContainer } from '../AiGeneration/AskAiEditorContainer'; import { renderResourcesEditorContainer } from './EditorContainers/ResourcesEditorContainer'; import { renderGlobalEventsSearchEditorContainer } from './EditorContainers/GlobalEventsSearchEditorContainer'; +import { type RenderEditorContainerPropsWithRef } from './EditorContainers/BaseEditor'; import { - type RenderEditorContainerPropsWithRef, type SceneEventsOutsideEditorChanges, type InstancesOutsideEditorChanges, type ObjectsOutsideEditorChanges, type ObjectGroupsOutsideEditorChanges, -} from './EditorContainers/BaseEditor'; + type SceneRenamedOutsideEditorChanges, +} from '../EditorFunctions/OutsideEditorChanges'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import ResourcesLoader from '../ResourcesLoader/index'; import { @@ -207,6 +208,7 @@ import useCreateProject, { type UseCreateProjectReturnType, } from '../Utils/UseCreateProject'; import newNameGenerator from '../Utils/NewNameGenerator'; +import { renameLayoutInProject } from '../Utils/Layout'; import { addDefaultLightToAllLayers } from '../ProjectCreation/CreateProject'; import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog'; import useEditorTabsStateSaving from './EditorTabs/UseEditorTabsStateSaving'; @@ -2088,23 +2090,10 @@ const MainFrame = (props: Props): React.MixedElement => { } ); - const layout = currentProject.getLayout(oldName); - const shouldChangeProjectFirstLayout = - oldName === currentProject.getFirstLayout(); - - // Rename first: the gdLayout pointer (and its instances/objects) is kept. - layout.setName(uniqueNewName); - gd.WholeProjectRefactorer.renameLayout( - currentProject, - oldName, - uniqueNewName - ); + renameLayoutInProject(currentProject, oldName, uniqueNewName); if (inAppTutorialOrchestratorRef.current) { inAppTutorialOrchestratorRef.current.changeData(oldName, uniqueNewName); } - if (shouldChangeProjectFirstLayout) { - currentProject.setFirstLayout(uniqueNewName); - } // External layout/events tabs are left untouched: they resolve the renamed // scene fresh on render. @@ -3664,6 +3653,27 @@ const MainFrame = (props: Props): React.MixedElement => { [state.editorTabs] ); + // The project model is already updated; just keep open tabs alive by renaming + // their project item. + const onSceneRenamedOutsideEditor = ( + changes: SceneRenamedOutsideEditorChanges + ) => { + const { oldName, newName } = changes; + setState(state => { + const { currentProject } = state; + if (!currentProject) return state; + return { + ...state, + editorTabs: getEditorTabsWithRenamedProjectItem( + state.editorTabs, + currentProject, + editorTab => + getRenamedLayoutTabProjectItemName(editorTab, oldName, newName) + ), + }; + }); + }; + const selectAllInActiveEditors = React.useCallback( () => { if (isUserTyping()) { @@ -5395,6 +5405,7 @@ const MainFrame = (props: Props): React.MixedElement => { onInstancesModifiedOutsideEditor: onInstancesModifiedOutsideEditor, onObjectsModifiedOutsideEditor: onObjectsModifiedOutsideEditor, onObjectGroupsModifiedOutsideEditor: onObjectGroupsModifiedOutsideEditor, + onSceneRenamedOutsideEditor: onSceneRenamedOutsideEditor, onWillInstallExtension: onWillInstallExtension, onExtensionInstalled: onExtensionInstalled, onEffectAdded: onEffectAdded, diff --git a/newIDE/app/src/Utils/Layout.js b/newIDE/app/src/Utils/Layout.js index c8c8edffa769..37e1f41b20b0 100644 --- a/newIDE/app/src/Utils/Layout.js +++ b/newIDE/app/src/Utils/Layout.js @@ -86,3 +86,17 @@ export const getInstanceCountInLayoutForObject = ( return getInstancesInLayoutForObject(initialInstancesContainer, objectName) .length; }; + +// `newName` is used as-is: ensure it is unique (e.g. via `newNameGenerator`). +export const renameLayoutInProject = ( + project: gdProject, + oldName: string, + newName: string +): void => { + const wasFirstScene = project.getFirstLayout() === oldName; + project.getLayout(oldName).setName(newName); + gd.WholeProjectRefactorer.renameLayout(project, oldName, newName); + if (wasFirstScene) { + project.setFirstLayout(newName); + } +}; diff --git a/newIDE/app/src/Utils/Layout.spec.js b/newIDE/app/src/Utils/Layout.spec.js new file mode 100644 index 000000000000..d00e4d26ed1b --- /dev/null +++ b/newIDE/app/src/Utils/Layout.spec.js @@ -0,0 +1,50 @@ +// @flow +import { renameLayoutInProject } from './Layout'; + +const gd: libGDevelop = global.gd; + +describe('renameLayoutInProject', () => { + const makeProject = () => { + // $FlowFixMe[invalid-constructor] + const project = new gd.ProjectHelper.createNewGDJSProject(); + project.insertNewLayout('Scene1', 0); + project.insertNewLayout('Scene2', 1); + return project; + }; + + it('renames the scene, keeping the same layout pointer', () => { + const project = makeProject(); + const scene = project.getLayout('Scene1'); + + renameLayoutInProject(project, 'Scene1', 'GameScene'); + + expect(project.hasLayoutNamed('Scene1')).toBe(false); + expect(project.hasLayoutNamed('GameScene')).toBe(true); + expect(scene.getName()).toBe('GameScene'); + expect(project.hasLayoutNamed('Scene2')).toBe(true); + + project.delete(); + }); + + it('preserves the first scene when the renamed scene was first', () => { + const project = makeProject(); + project.setFirstLayout('Scene1'); + + renameLayoutInProject(project, 'Scene1', 'GameScene'); + + expect(project.getFirstLayout()).toBe('GameScene'); + + project.delete(); + }); + + it('leaves the first scene untouched when renaming another scene', () => { + const project = makeProject(); + project.setFirstLayout('Scene1'); + + renameLayoutInProject(project, 'Scene2', 'OtherScene'); + + expect(project.getFirstLayout()).toBe('Scene1'); + + project.delete(); + }); +});