Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
c0ab3ccc50 Refactor MainFrame to extract EditorsPane component
Co-authored-by: florian <florian@gdevelop.io>
2025-07-13 19:00:06 +00:00
2 changed files with 321 additions and 499 deletions

View File

@@ -2,264 +2,66 @@
import * as React from 'react';
import TabsTitlebar from './TabsTitlebar';
import Toolbar, { type ToolbarInterface } from './Toolbar';
import { TabContentContainer } from '../UI/ClosableTabs';
import { DraggableEditorTabs } from './EditorTabs/DraggableEditorTabs';
import CommandsContextScopedProvider from '../CommandPalette/CommandsScopedContext';
import ErrorBoundary, {
getEditorErrorBoundaryProps,
} from '../UI/ErrorBoundary';
import {
getEditors,
getCurrentTabIndex,
getCurrentTab,
getCurrentTabIndex,
type EditorTabsState,
type EditorTab,
hasEditorTabOpenedWithKey,
changeCurrentTab,
closeEditorTab,
closeOtherEditorTabs,
closeAllEditorTabs,
moveTabToTheRightOfHoveredTab,
saveUiSettings,
} from './EditorTabs/EditorTabsHandler';
import type { PreviewState } from './PreviewState';
import type {
RenderEditorContainerPropsWithRef,
SceneEventsOutsideEditorChanges,
} from './EditorContainers/BaseEditor';
import type { ResourceManagementProps } from '../ResourcesList/ResourceSource';
import type { HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
import type { GamesList } from '../GameDashboard/UseGamesList';
import type { GamesPlatformFrameTools } from './EditorContainers/HomePage/PlaySection/UseGamesPlatformFrame';
import type { FileMetadata } from '../ProjectsStorage';
import UnsavedChangesContext from './UnsavedChangesContext';
import type { InAppTutorialOrchestrator } from '../InAppTutorial/InAppTutorialOrchestrator';
import type { VersionHistoryPanelStatus } from '../VersionHistory/UseVersionHistory';
const gd: libGDevelop = global.gd;
/*
* EditorsPane is a thin wrapper that gathers together:
* - the TabsTitlebar (and the draggable tabs inside it)
* - the main Toolbar
* - the rendering of the editors contained in the current pane.
*
* It is intentionally kept “dumb”: all the real logic still sits in
* MainFrame, and is passed down through props. This allows MainFrame to
* continue owning the business logic while giving us the flexibility to
* later instantiate several panes or move tabs between them.
*/
type Props = {|
// State of the tabs for this pane
editorTabs: EditorTabsState,
currentProject: ?gdProject,
currentFileMetadata: ?FileMetadata,
// Visibility/hide logic shared with MainFrame
tabsTitleBarAndEditorToolbarHidden: boolean,
setTabsTitleBarAndEditorToolbarHidden: (hidden: boolean) => void,
canSave: boolean,
isSavingProject: boolean,
isSharingEnabled: boolean,
hasPreviewsRunning: boolean,
previewState: PreviewState,
checkedOutVersionStatus: VersionHistoryPanelStatus,
canDoNetworkPreview: boolean,
gamesPlatformFrameTools: GamesPlatformFrameTools,
// Callbacks from MainFrame
// Title-bar props
toggleProjectManager: () => void,
saveProject: () => Promise<void>,
openShareDialog: (tab?: string) => void,
launchDebuggerAndPreview: () => void,
launchNewPreview: (options?: {| networkPreview?: boolean |}) => void,
launchNetworkPreview: () => void,
launchHotReloadPreview: () => void,
launchPreviewWithDiagnosticReport: () => void,
setPreviewOverride: (override: {|
isPreviewOverriden: boolean,
overridenPreviewLayoutName: ?string,
overridenPreviewExternalLayoutName: ?string,
|}) => void,
openVersionHistoryPanel: () => void,
onQuitVersionHistory: () => void,
openAskAi: () => void,
getStorageProvider: () => any,
setPreviewedLayout: (layoutName: ?string) => void,
openExternalEvents: (name: string) => void,
openLayout: (name: string, options?: any) => void,
openTemplateFromTutorial: any => void,
openTemplateFromCourseChapter: any => void,
previewDebuggerServer: ?any,
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
resourceManagementProps: ResourceManagementProps,
onCreateEventsFunction: any,
openInstructionOrExpression: any,
onOpenCustomObjectEditor: any => void,
onRenamedEventsBasedObject: any => void,
onDeletedEventsBasedObject: any => void,
openObjectEvents: any,
canOpen: boolean,
openOpenFromStorageProviderDialog: () => void,
openFromFileMetadataWithStorageProvider: any => void,
openNewProjectDialog: () => void,
openProjectManager: (open: boolean) => void,
askToCloseProject: () => Promise<boolean>,
closeProject: () => Promise<void>,
onSelectExampleShortHeader: any => void,
onSelectPrivateGameTemplateListingData: any => void,
createEmptyProject: any,
createProjectFromExample: any,
onOpenProfileDialog: () => void,
openLanguageDialog: (open: boolean) => void,
openPreferencesDialog: (open: boolean) => void,
openAboutDialog: (open: boolean) => void,
selectInAppTutorial: any => void,
eventsFunctionsExtensionsState: any,
isProjectClosedSoAvoidReloadingExtensions: boolean,
renameResourcesInProject: (project: gdProject, renames: {[string]: string}) => void,
openBehaviorEvents: any,
onExtractAsExternalLayout: any,
onOpenEventBasedObjectEditor: any,
onOpenEventBasedObjectVariantEditor: any,
deleteEventsBasedObjectVariant: any,
onEventsBasedObjectChildrenEdited: any,
onSceneObjectEdited: any,
onSceneObjectsDeleted: any,
onSceneEventsModifiedOutsideEditor: any,
onExtensionInstalled: any,
gamesList: GamesList,
inAppTutorialOrchestratorRef: {| current: ?InAppTutorialOrchestrator |},
setEditorTabs: (editorTabs: EditorTabsState) => void,
hasAskAiOpened: boolean,
onOpenAskAi: () => void,
// Tab actions
onChangeEditorTab: (id: number) => void,
onCloseEditorTab: (editorTab: EditorTab) => void,
onCloseOtherEditorTabs: (editorTab: EditorTab) => void,
onCloseAllEditorTabs: () => void,
onEditorTabActivated: (editorTab: EditorTab) => void,
onDropEditorTab: (sourceEditorTab: EditorTab, destinationEditorTab: EditorTab) => void,
// Toolbar props
toolbarRef: { current: ?ToolbarInterface },
toolbarProps: Object,
// Renderer for a single editor tab content (kept in MainFrame)
renderEditorTab: (editorTab: EditorTab, isCurrentTab: boolean) => React.Node,
|};
const EditorsPane = React.forwardRef<Props, ToolbarInterface>((props, ref) => {
const {
editorTabs,
currentProject,
currentFileMetadata,
tabsTitleBarAndEditorToolbarHidden,
setTabsTitleBarAndEditorToolbarHidden,
canSave,
isSavingProject,
isSharingEnabled,
hasPreviewsRunning,
previewState,
checkedOutVersionStatus,
canDoNetworkPreview,
gamesPlatformFrameTools,
toggleProjectManager,
saveProject,
openShareDialog,
launchDebuggerAndPreview,
launchNewPreview,
launchNetworkPreview,
launchHotReloadPreview,
launchPreviewWithDiagnosticReport,
setPreviewOverride,
openVersionHistoryPanel,
onQuitVersionHistory,
openAskAi,
getStorageProvider,
setPreviewedLayout,
openExternalEvents,
openLayout,
openTemplateFromTutorial,
openTemplateFromCourseChapter,
previewDebuggerServer,
hotReloadPreviewButtonProps,
resourceManagementProps,
onCreateEventsFunction,
openInstructionOrExpression,
onOpenCustomObjectEditor,
onRenamedEventsBasedObject,
onDeletedEventsBasedObject,
openObjectEvents,
canOpen,
openOpenFromStorageProviderDialog,
openFromFileMetadataWithStorageProvider,
openNewProjectDialog,
openProjectManager,
askToCloseProject,
closeProject,
onSelectExampleShortHeader,
onSelectPrivateGameTemplateListingData,
createEmptyProject,
createProjectFromExample,
onOpenProfileDialog,
openLanguageDialog,
openPreferencesDialog,
openAboutDialog,
selectInAppTutorial,
eventsFunctionsExtensionsState,
isProjectClosedSoAvoidReloadingExtensions,
renameResourcesInProject,
openBehaviorEvents,
onExtractAsExternalLayout,
onOpenEventBasedObjectEditor,
onOpenEventBasedObjectVariantEditor,
deleteEventsBasedObjectVariant,
onEventsBasedObjectChildrenEdited,
onSceneObjectEdited,
onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor,
onExtensionInstalled,
gamesList,
inAppTutorialOrchestratorRef,
setEditorTabs,
} = props;
const toolbarRef = React.useRef<?ToolbarInterface>(null);
const unsavedChanges = React.useContext(UnsavedChangesContext);
const hasAskAiOpened = hasEditorTabOpenedWithKey(editorTabs, 'ask-ai');
// Internal editor toolbar management
const setEditorToolbar = React.useCallback((editorToolbar: any, isCurrentTab: boolean = true) => {
if (!toolbarRef.current || !isCurrentTab) return;
toolbarRef.current.setEditorToolbar(editorToolbar);
}, []);
const updateToolbar = React.useCallback(() => {
const editorTab = getCurrentTab(editorTabs);
if (!editorTab || !editorTab.editorRef) {
setEditorToolbar(null);
return;
}
editorTab.editorRef.updateToolbar();
}, [editorTabs, setEditorToolbar]);
React.useEffect(() => {
updateToolbar();
}, [updateToolbar]);
// Tab management functions
const _onEditorTabActivated = React.useCallback((editorTab: EditorTab) => {
updateToolbar();
// Ensure the editors shown on the screen are updated. This is for
// example useful if global objects have been updated in another editor.
if (editorTab.editorRef) {
editorTab.editorRef.forceUpdateEditor();
}
}, [updateToolbar]);
const _onChangeEditorTab = React.useCallback((value: number) => {
const newEditorTabs = changeCurrentTab(editorTabs, value);
setEditorTabs(newEditorTabs);
_onEditorTabActivated(getCurrentTab(newEditorTabs));
}, [editorTabs, setEditorTabs, _onEditorTabActivated]);
const _onCloseEditorTab = React.useCallback((editorTab: EditorTab) => {
saveUiSettings(editorTabs);
setEditorTabs(closeEditorTab(editorTabs, editorTab));
}, [editorTabs, setEditorTabs]);
const _onCloseOtherEditorTabs = React.useCallback((editorTab: EditorTab) => {
saveUiSettings(editorTabs);
setEditorTabs(closeOtherEditorTabs(editorTabs, editorTab));
}, [editorTabs, setEditorTabs]);
const _onCloseAllEditorTabs = React.useCallback(() => {
saveUiSettings(editorTabs);
setEditorTabs(closeAllEditorTabs(editorTabs));
}, [editorTabs, setEditorTabs]);
const onDropEditorTab = React.useCallback((fromIndex: number, toHoveredIndex: number) => {
setEditorTabs(moveTabToTheRightOfHoveredTab(editorTabs, fromIndex, toHoveredIndex));
}, [editorTabs, setEditorTabs]);
// Expose toolbar interface methods
React.useImperativeHandle(ref, () => ({
setEditorToolbar,
}), [setEditorToolbar]);
const EditorsPane = ({
editorTabs,
tabsTitleBarAndEditorToolbarHidden,
toggleProjectManager,
hasAskAiOpened,
onOpenAskAi,
onChangeEditorTab,
onCloseEditorTab,
onCloseOtherEditorTabs,
onCloseAllEditorTabs,
onEditorTabActivated,
onDropEditorTab,
toolbarRef,
toolbarProps,
renderEditorTab,
}: Props): React.Node => {
return (
<>
<TabsTitlebar
@@ -269,24 +71,21 @@ const EditorsPane = React.forwardRef<Props, ToolbarInterface>((props, ref) => {
<DraggableEditorTabs
hideLabels={false}
editorTabs={editorTabs}
onClickTab={(id: number) => _onChangeEditorTab(id)}
onClickTab={(id: number) => onChangeEditorTab(id)}
onCloseTab={(editorTab: EditorTab) => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseEditorTab(editorTab);
onCloseEditorTab(editorTab);
}}
onCloseOtherTabs={(editorTab: EditorTab) => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseOtherEditorTabs(editorTab);
onCloseOtherEditorTabs(editorTab);
}}
onCloseAll={() => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseAllEditorTabs();
onCloseAllEditorTabs();
}}
onTabActivated={(editorTab: EditorTab) =>
_onEditorTabActivated(editorTab)
onEditorTabActivated(editorTab)
}
onDropTab={onDropEditorTab}
onHoverTab={(
@@ -296,183 +95,23 @@ const EditorsPane = React.forwardRef<Props, ToolbarInterface>((props, ref) => {
/>
)}
hasAskAiOpened={hasAskAiOpened}
onOpenAskAi={openAskAi}
onOpenAskAi={onOpenAskAi}
/>
<Toolbar
ref={toolbarRef}
hidden={tabsTitleBarAndEditorToolbarHidden}
showProjectButtons={
!['start page', 'debugger', 'ask-ai', null].includes(
getCurrentTab(editorTabs)
? getCurrentTab(editorTabs).key
: null
getCurrentTab(editorTabs) ? getCurrentTab(editorTabs).key : null
)
}
canSave={canSave}
onSave={saveProject}
openShareDialog={() =>
openShareDialog(/* leave the dialog decide which tab to open */)
}
isSharingEnabled={isSharingEnabled}
onOpenDebugger={launchDebuggerAndPreview}
hasPreviewsRunning={hasPreviewsRunning}
onPreviewWithoutHotReload={launchNewPreview}
onNetworkPreview={launchNetworkPreview}
onHotReloadPreview={launchHotReloadPreview}
onLaunchPreviewWithDiagnosticReport={launchPreviewWithDiagnosticReport}
canDoNetworkPreview={canDoNetworkPreview}
setPreviewOverride={setPreviewOverride}
isPreviewEnabled={
!!currentProject && currentProject.getLayoutsCount() > 0
}
previewState={previewState}
onOpenVersionHistory={openVersionHistoryPanel}
checkedOutVersionStatus={checkedOutVersionStatus}
onQuitVersionHistory={onQuitVersionHistory}
canQuitVersionHistory={!isSavingProject}
{...toolbarProps}
/>
{getEditors(editorTabs).map((editorTab, id) => {
const isCurrentTab = getCurrentTabIndex(editorTabs) === id;
const errorBoundaryProps = getEditorErrorBoundaryProps(editorTab.key);
return (
<TabContentContainer
key={editorTab.key}
active={isCurrentTab}
// Deactivate pointer events when the play tab is active, so the iframe
// can be interacted with.
removePointerEvents={gamesPlatformFrameTools.iframeVisible}
>
<CommandsContextScopedProvider active={isCurrentTab}>
<ErrorBoundary
componentTitle={errorBoundaryProps.componentTitle}
scope={errorBoundaryProps.scope}
>
{editorTab.renderEditorContainer({
isActive: isCurrentTab,
extraEditorProps: editorTab.extraEditorProps,
project: currentProject,
fileMetadata: currentFileMetadata,
storageProvider: getStorageProvider(),
ref: editorRef => (editorTab.editorRef = editorRef),
setToolbar: editorToolbar =>
setEditorToolbar(editorToolbar, isCurrentTab),
hideTabsTitleBarAndEditorToolbar: setTabsTitleBarAndEditorToolbarHidden,
projectItemName: editorTab.projectItemName,
setPreviewedLayout,
onOpenExternalEvents: openExternalEvents,
onOpenEvents: (sceneName: string) => {
openLayout(sceneName, {
openEventsEditor: true,
openSceneEditor: false,
focusWhenOpened: 'events',
});
},
onOpenLayout: openLayout,
onOpenTemplateFromTutorial: openTemplateFromTutorial,
onOpenTemplateFromCourseChapter: openTemplateFromCourseChapter,
previewDebuggerServer,
hotReloadPreviewButtonProps,
resourceManagementProps,
onSave: saveProject,
canSave,
onCreateEventsFunction,
openInstructionOrExpression,
onOpenCustomObjectEditor: onOpenCustomObjectEditor,
onRenamedEventsBasedObject: onRenamedEventsBasedObject,
onDeletedEventsBasedObject: onDeletedEventsBasedObject,
openObjectEvents,
unsavedChanges: unsavedChanges,
canOpen,
onChooseProject: () => openOpenFromStorageProviderDialog(),
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
onOpenNewProjectSetupDialog: openNewProjectDialog,
onOpenProjectManager: () => openProjectManager(true),
onOpenVersionHistory: openVersionHistoryPanel,
askToCloseProject,
closeProject,
onSelectExampleShortHeader: exampleShortHeader => {
onSelectExampleShortHeader({
exampleShortHeader,
preventBackHome: true,
});
},
onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => {
onSelectPrivateGameTemplateListingData({
privateGameTemplateListingData,
preventBackHome: true,
});
},
onOpenPrivateGameTemplateListingData: privateGameTemplateListingData => {
onSelectPrivateGameTemplateListingData({
privateGameTemplateListingData,
preventBackHome: true,
});
},
onCreateEmptyProject: createEmptyProject,
onCreateProjectFromExample: createProjectFromExample,
onOpenProfile: onOpenProfileDialog,
onOpenLanguageDialog: () => openLanguageDialog(true),
onOpenPreferences: () => openPreferencesDialog(true),
onOpenAbout: () => openAboutDialog(true),
selectInAppTutorial: selectInAppTutorial,
onLoadEventsFunctionsExtensions: async () => {
if (isProjectClosedSoAvoidReloadingExtensions) {
return;
}
return eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions(
currentProject
);
},
onReloadEventsFunctionsExtensionMetadata: extension => {
if (isProjectClosedSoAvoidReloadingExtensions) {
return;
}
eventsFunctionsExtensionsState.reloadProjectEventsFunctionsExtensionMetadata(
currentProject,
extension
);
},
onDeleteResource: (
resource: gdResource,
cb: boolean => void
) => {
// TODO: Project wide refactoring of objects/events using the resource
cb(true);
},
onRenameResource: (
resource: gdResource,
newName: string,
cb: boolean => void
) => {
if (currentProject)
renameResourcesInProject(currentProject, {
[resource.getName()]: newName,
});
cb(true);
},
openBehaviorEvents: openBehaviorEvents,
onExtractAsExternalLayout: onExtractAsExternalLayout,
onExtractAsEventBasedObject: onOpenEventBasedObjectEditor,
onOpenEventBasedObjectEditor: onOpenEventBasedObjectEditor,
onOpenEventBasedObjectVariantEditor: onOpenEventBasedObjectVariantEditor,
onDeleteEventsBasedObjectVariant: deleteEventsBasedObjectVariant,
onEventsBasedObjectChildrenEdited: onEventsBasedObjectChildrenEdited,
onSceneObjectEdited: onSceneObjectEdited,
onSceneObjectsDeleted: onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor: onSceneEventsModifiedOutsideEditor,
onExtensionInstalled: onExtensionInstalled,
gamesList,
gamesPlatformFrameTools,
})}
</ErrorBoundary>
</CommandsContextScopedProvider>
</TabContentContainer>
);
})}
{getEditors(editorTabs).map((editorTab, id) =>
renderEditorTab(editorTab, getCurrentTabIndex(editorTabs) === id)
)}
</>
);
});
};
export default EditorsPane;

View File

@@ -23,10 +23,13 @@ import Window from '../Utils/Window';
import { showErrorBox } from '../UI/Messages/MessageBox';
import { TabContentContainer } from '../UI/ClosableTabs';
import { DraggableEditorTabs } from './EditorTabs/DraggableEditorTabs';
import EditorsPane from './EditorsPane';
import {
getEditorTabsInitialState,
openEditorTab,
closeEditorTab,
closeOtherEditorTabs,
closeAllEditorTabs,
changeCurrentTab,
getEditors,
getCurrentTabIndex,
getCurrentTab,
@@ -43,6 +46,7 @@ import {
type EditorKind,
getEventsFunctionsExtensionEditor,
notifyPreviewOrExportWillStart,
moveTabToTheRightOfHoveredTab,
getCustomObjectEditor,
hasEditorTabOpenedWithKey,
getOpenedAskAiEditor,
@@ -170,6 +174,7 @@ import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext';
import useOpenInitialDialog from '../Utils/UseOpenInitialDialog';
import { type InAppTutorialOrchestratorInterface } from '../InAppTutorial/InAppTutorialOrchestrator';
import useInAppTutorialOrchestrator from '../InAppTutorial/useInAppTutorialOrchestrator';
import TabsTitlebar from './TabsTitlebar';
import {
useStableUpToDateCallback,
useStableUpToDateRef,
@@ -203,6 +208,7 @@ import RobotIcon from '../ProjectCreation/RobotIcon';
import PublicProfileContext from '../Profile/PublicProfileContext';
import { useGamesPlatformFrame } from './EditorContainers/HomePage/PlaySection/UseGamesPlatformFrame';
import { useExtensionLoadErrorDialog } from '../Utils/UseExtensionLoadErrorDialog';
import EditorsPane from './EditorsPane';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
@@ -349,7 +355,7 @@ const MainFrame = (props: Props) => {
gdjsDevelopmentWatcherEnabled: false,
}: State)
);
const editorsPaneRef = React.useRef<?ToolbarInterface>(null);
const toolbar = React.useRef<?ToolbarInterface>(null);
const [
tabsTitleBarAndEditorToolbarHidden,
setTabsTitleBarAndEditorToolbarHidden,
@@ -768,7 +774,25 @@ const MainFrame = (props: Props) => {
});
};
const updateToolbar = React.useCallback(
(newEditorTabs = state.editorTabs) => {
const editorTab = getCurrentTab(newEditorTabs);
if (!editorTab || !editorTab.editorRef) {
setEditorToolbar(null);
return;
}
editorTab.editorRef.updateToolbar();
},
[state.editorTabs]
);
React.useEffect(
() => {
updateToolbar();
},
[updateToolbar]
);
const _languageDidChange = () => {
// A change in the language will automatically be applied
@@ -1261,12 +1285,18 @@ const MainFrame = (props: Props) => {
const toggleProjectManager = React.useCallback(
() => {
if (editorsPaneRef.current)
if (toolbar.current)
openProjectManager(projectManagerOpen => !projectManagerOpen);
},
[openProjectManager]
);
const setEditorToolbar = (editorToolbar: any, isCurrentTab = true) => {
if (!toolbar.current || !isCurrentTab) return;
toolbar.current.setEditorToolbar(editorToolbar);
};
const deleteLayout = (layout: gdLayout) => {
const { currentProject } = state;
const { i18n } = props;
@@ -3220,7 +3250,61 @@ const MainFrame = (props: Props) => {
[currentProject, hasUnsavedChanges, i18n, closeProject]
);
const _onChangeEditorTab = (value: number) => {
setState(state => ({
...state,
editorTabs: changeCurrentTab(state.editorTabs, value),
})).then(state =>
_onEditorTabActivated(getCurrentTab(state.editorTabs), state)
);
};
const _onEditorTabActivated = (
editorTab: EditorTab,
newState: State = state
) => {
updateToolbar(newState.editorTabs);
// Ensure the editors shown on the screen are updated. This is for
// example useful if global objects have been updated in another editor.
if (editorTab.editorRef) {
editorTab.editorRef.forceUpdateEditor();
}
};
const _onCloseEditorTab = (editorTab: EditorTab) => {
saveUiSettings(state.editorTabs);
setState(state => ({
...state,
editorTabs: closeEditorTab(state.editorTabs, editorTab),
}));
};
const _onCloseOtherEditorTabs = (editorTab: EditorTab) => {
saveUiSettings(state.editorTabs);
setState(state => ({
...state,
editorTabs: closeOtherEditorTabs(state.editorTabs, editorTab),
}));
};
const _onCloseAllEditorTabs = () => {
saveUiSettings(state.editorTabs);
setState(state => ({
...state,
editorTabs: closeAllEditorTabs(state.editorTabs),
}));
};
const onDropEditorTab = (fromIndex: number, toHoveredIndex: number) => {
setState(state => ({
...state,
editorTabs: moveTabToTheRightOfHoveredTab(
state.editorTabs,
fromIndex,
toHoveredIndex
),
}));
};
const endTutorial = React.useCallback(
async (shouldCloseProject?: boolean) => {
@@ -3752,6 +3836,150 @@ const MainFrame = (props: Props) => {
(!currentFileMetadata || !isProjectOwnedBySomeoneElse);
const hasAskAiOpened = hasEditorTabOpenedWithKey(state.editorTabs, 'ask-ai');
// Render a single editor tab content. This used to be inline in the JSX but
// has been extracted so that the EditorsPane component can be kept generic.
const renderEditorTab = (
editorTab: EditorTab,
isCurrentTab: boolean
): React.Node => {
const errorBoundaryProps = getEditorErrorBoundaryProps(editorTab.key);
return (
<TabContentContainer
key={editorTab.key}
active={isCurrentTab}
removePointerEvents={gamesPlatformFrameTools.iframeVisible}
>
<CommandsContextScopedProvider active={isCurrentTab}>
<ErrorBoundary
componentTitle={errorBoundaryProps.componentTitle}
scope={errorBoundaryProps.scope}
>
{editorTab.renderEditorContainer({
isActive: isCurrentTab,
extraEditorProps: editorTab.extraEditorProps,
project: currentProject,
fileMetadata: currentFileMetadata,
storageProvider: getStorageProvider(),
ref: editorRef => (editorTab.editorRef = editorRef),
setToolbar: editorToolbar =>
setEditorToolbar(editorToolbar, isCurrentTab),
hideTabsTitleBarAndEditorToolbar: setTabsTitleBarAndEditorToolbarHidden,
projectItemName: editorTab.projectItemName,
setPreviewedLayout,
onOpenExternalEvents: openExternalEvents,
onOpenEvents: (sceneName: string) => {
openLayout(sceneName, {
openEventsEditor: true,
openSceneEditor: false,
focusWhenOpened: 'events',
});
},
onOpenLayout: openLayout,
onOpenTemplateFromTutorial: openTemplateFromTutorial,
onOpenTemplateFromCourseChapter: openTemplateFromCourseChapter,
previewDebuggerServer,
hotReloadPreviewButtonProps,
resourceManagementProps,
onSave: saveProject,
canSave,
onCreateEventsFunction,
openInstructionOrExpression,
onOpenCustomObjectEditor: openCustomObjectEditor,
onRenamedEventsBasedObject: onRenamedEventsBasedObject,
onDeletedEventsBasedObject: onDeletedEventsBasedObject,
openObjectEvents,
unsavedChanges: unsavedChanges,
canOpen: !!props.storageProviders.filter(
({ hiddenInOpenDialog }) => !hiddenInOpenDialog
).length,
onChooseProject: () => openOpenFromStorageProviderDialog(),
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
onOpenNewProjectSetupDialog: openNewProjectDialog,
onOpenProjectManager: () => openProjectManager(true),
onOpenVersionHistory: openVersionHistoryPanel,
askToCloseProject,
closeProject,
onSelectExampleShortHeader: exampleShortHeader => {
onSelectExampleShortHeader({
exampleShortHeader,
preventBackHome: true,
});
},
onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => {
onSelectPrivateGameTemplateListingData({
privateGameTemplateListingData,
preventBackHome: true,
});
},
onOpenPrivateGameTemplateListingData: privateGameTemplateListingData => {
onSelectPrivateGameTemplateListingData({
privateGameTemplateListingData,
preventBackHome: true,
});
},
onCreateEmptyProject: createEmptyProject,
onCreateProjectFromExample: createProjectFromExample,
onOpenProfile: onOpenProfileDialog,
onOpenLanguageDialog: () => openLanguageDialog(true),
onOpenPreferences: () => openPreferencesDialog(true),
onOpenAbout: () => openAboutDialog(true),
selectInAppTutorial: selectInAppTutorial,
onLoadEventsFunctionsExtensions: async () => {
if (isProjectClosedSoAvoidReloadingExtensions) {
return;
}
return eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions(
currentProject
);
},
onReloadEventsFunctionsExtensionMetadata: extension => {
if (isProjectClosedSoAvoidReloadingExtensions) {
return;
}
eventsFunctionsExtensionsState.reloadProjectEventsFunctionsExtensionMetadata(
currentProject,
extension
);
},
onDeleteResource: (
resource: gdResource,
cb: boolean => void
) => {
cb(true);
},
onRenameResource: (
resource: gdResource,
newName: string,
cb: boolean => void
) => {
if (currentProject)
renameResourcesInProject(currentProject, {
[resource.getName()]: newName,
});
cb(true);
},
openBehaviorEvents: openBehaviorEvents,
onExtractAsExternalLayout: onExtractAsExternalLayout,
onExtractAsEventBasedObject: onOpenEventBasedObjectEditor,
onOpenEventBasedObjectEditor: onOpenEventBasedObjectEditor,
onOpenEventBasedObjectVariantEditor: onOpenEventBasedObjectVariantEditor,
onDeleteEventsBasedObjectVariant: deleteEventsBasedObjectVariant,
onEventsBasedObjectChildrenEdited: onEventsBasedObjectChildrenEdited,
onSceneObjectEdited: onSceneObjectEdited,
onSceneObjectsDeleted: onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor: onSceneEventsModifiedOutsideEditor,
onExtensionInstalled: onExtensionInstalled,
gamesList,
gamesPlatformFrameTools,
})}
</ErrorBoundary>
</CommandsContextScopedProvider>
</TabContentContainer>
);
};
return (
<div
className={
@@ -3826,89 +4054,44 @@ const MainFrame = (props: Props) => {
// in what to display (ex: Loader of play section)
gamesPlatformFrameTools.renderGamesPlatformFrame()}
<LeaderboardProvider
gameId={
state.currentProject ? state.currentProject.getProjectUuid() : ''
}
gameId={state.currentProject ? state.currentProject.getProjectUuid() : ''}
>
<EditorsPane
ref={editorsPaneRef}
editorTabs={state.editorTabs}
currentProject={currentProject}
currentFileMetadata={currentFileMetadata}
tabsTitleBarAndEditorToolbarHidden={tabsTitleBarAndEditorToolbarHidden}
setTabsTitleBarAndEditorToolbarHidden={setTabsTitleBarAndEditorToolbarHidden}
canSave={canSave}
isSavingProject={isSavingProject}
isSharingEnabled={!checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId}
hasPreviewsRunning={hasPreviewsRunning}
previewState={previewState}
checkedOutVersionStatus={checkedOutVersionStatus}
canDoNetworkPreview={
!!_previewLauncher.current &&
_previewLauncher.current.canDoNetworkPreview()
}
gamesPlatformFrameTools={gamesPlatformFrameTools}
toggleProjectManager={toggleProjectManager}
setEditorTabs={setEditorTabs}
saveProject={saveProject}
openShareDialog={openShareDialog}
launchDebuggerAndPreview={launchDebuggerAndPreview}
launchNewPreview={launchNewPreview}
launchNetworkPreview={launchNetworkPreview}
launchHotReloadPreview={launchHotReloadPreview}
launchPreviewWithDiagnosticReport={launchPreviewWithDiagnosticReport}
setPreviewOverride={setPreviewOverride}
openVersionHistoryPanel={openVersionHistoryPanel}
onQuitVersionHistory={onQuitVersionHistory}
openAskAi={openAskAi}
getStorageProvider={getStorageProvider}
setPreviewedLayout={setPreviewedLayout}
openExternalEvents={openExternalEvents}
openLayout={openLayout}
openTemplateFromTutorial={openTemplateFromTutorial}
openTemplateFromCourseChapter={openTemplateFromCourseChapter}
previewDebuggerServer={previewDebuggerServer}
hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
resourceManagementProps={resourceManagementProps}
onCreateEventsFunction={onCreateEventsFunction}
openInstructionOrExpression={openInstructionOrExpression}
onOpenCustomObjectEditor={openCustomObjectEditor}
onRenamedEventsBasedObject={onRenamedEventsBasedObject}
onDeletedEventsBasedObject={onDeletedEventsBasedObject}
openObjectEvents={openObjectEvents}
canOpen={!!props.storageProviders.filter(
({ hiddenInOpenDialog }) => !hiddenInOpenDialog
).length}
openOpenFromStorageProviderDialog={openOpenFromStorageProviderDialog}
openFromFileMetadataWithStorageProvider={openFromFileMetadataWithStorageProvider}
openNewProjectDialog={openNewProjectDialog}
openProjectManager={openProjectManager}
askToCloseProject={askToCloseProject}
closeProject={closeProject}
onSelectExampleShortHeader={onSelectExampleShortHeader}
onSelectPrivateGameTemplateListingData={onSelectPrivateGameTemplateListingData}
createEmptyProject={createEmptyProject}
createProjectFromExample={createProjectFromExample}
onOpenProfileDialog={onOpenProfileDialog}
openLanguageDialog={openLanguageDialog}
openPreferencesDialog={openPreferencesDialog}
openAboutDialog={openAboutDialog}
selectInAppTutorial={selectInAppTutorial}
eventsFunctionsExtensionsState={eventsFunctionsExtensionsState}
isProjectClosedSoAvoidReloadingExtensions={isProjectClosedSoAvoidReloadingExtensions}
renameResourcesInProject={renameResourcesInProject}
openBehaviorEvents={openBehaviorEvents}
onExtractAsExternalLayout={onExtractAsExternalLayout}
onOpenEventBasedObjectEditor={onOpenEventBasedObjectEditor}
onOpenEventBasedObjectVariantEditor={onOpenEventBasedObjectVariantEditor}
deleteEventsBasedObjectVariant={deleteEventsBasedObjectVariant}
onEventsBasedObjectChildrenEdited={onEventsBasedObjectChildrenEdited}
onSceneObjectEdited={onSceneObjectEdited}
onSceneObjectsDeleted={onSceneObjectsDeleted}
onSceneEventsModifiedOutsideEditor={onSceneEventsModifiedOutsideEditor}
onExtensionInstalled={onExtensionInstalled}
gamesList={gamesList}
inAppTutorialOrchestratorRef={inAppTutorialOrchestratorRef}
hasAskAiOpened={hasAskAiOpened}
onOpenAskAi={openAskAi}
onChangeEditorTab={_onChangeEditorTab}
onCloseEditorTab={_onCloseEditorTab}
onCloseOtherEditorTabs={_onCloseOtherEditorTabs}
onCloseAllEditorTabs={_onCloseAllEditorTabs}
onEditorTabActivated={_onEditorTabActivated}
onDropEditorTab={onDropEditorTab}
toolbarRef={toolbar}
toolbarProps={{
canSave,
onSave: saveProject,
openShareDialog: () =>
openShareDialog(/* leave the dialog decide which tab to open */),
isSharingEnabled: !checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId,
onOpenDebugger: launchDebuggerAndPreview,
hasPreviewsRunning,
onPreviewWithoutHotReload: launchNewPreview,
onNetworkPreview: launchNetworkPreview,
onHotReloadPreview: launchHotReloadPreview,
onLaunchPreviewWithDiagnosticReport: launchPreviewWithDiagnosticReport,
canDoNetworkPreview:
!!_previewLauncher.current && _previewLauncher.current.canDoNetworkPreview(),
setPreviewOverride,
isPreviewEnabled: !!currentProject && currentProject.getLayoutsCount() > 0,
previewState,
onOpenVersionHistory: openVersionHistoryPanel,
checkedOutVersionStatus,
onQuitVersionHistory,
canQuitVersionHistory: !isSavingProject,
}}
renderEditorTab={renderEditorTab}
/>
</LeaderboardProvider>
<CommandPaletteWithAlgoliaSearch ref={commandPaletteRef} />