Add crude working version of panes

This commit is contained in:
Florian Rival
2025-07-14 00:29:33 +02:00
parent aded08471d
commit ede984d605
9 changed files with 1165 additions and 448 deletions

View File

@@ -3,8 +3,10 @@
import * as React from 'react';
import InAppTutorialOrchestrator from './InAppTutorialOrchestrator';
import { type EditorIdentifier } from '../Utils/GDevelopServices/InAppTutorial';
import { type EditorTabsState } from '../MainFrame/EditorTabs/EditorTabsHandler';
import { getCurrentTab } from '../MainFrame/EditorTabs/EditorTabsHandler';
import {
getCurrentTabForPane,
type EditorTabsState,
} from '../MainFrame/EditorTabs/EditorTabsHandler';
type Props = {|
editorTabs: EditorTabsState,
@@ -21,7 +23,7 @@ const useInAppTutorialOrchestrator = ({ editorTabs }: Props) => {
React.useEffect(
() => {
const currentTab = getCurrentTab(editorTabs);
const currentTab = getCurrentTabForPane(editorTabs, 'center');
if (!currentTab) {
setCurrentEditor(null);
return;

View File

@@ -4,13 +4,7 @@ import * as React from 'react';
import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndDropTarget';
import { ScreenTypeMeasurer } from '../../UI/Responsive/ScreenTypeMeasurer';
import { ColumnDropIndicator } from './DropIndicator';
import {
type EditorTabsState,
type EditorTab,
getEditors,
getCurrentTabIndex,
getCurrentTab,
} from './EditorTabsHandler';
import { type EditorTab } from './EditorTabsHandler';
import {
ClosableTabs,
ClosableTab,
@@ -26,7 +20,8 @@ const DragSourceAndDropTarget = makeDragSourceAndDropTarget<EditorTab>(
type DraggableEditorTabsProps = {|
hideLabels?: boolean,
editorTabs: EditorTabsState,
editors: Array<EditorTab>,
currentTab: EditorTab | null,
onClickTab: (index: number) => void,
onCloseTab: (editor: EditorTab) => void,
onCloseOtherTabs: (editor: EditorTab) => void,
@@ -46,7 +41,8 @@ const homeTabApproximateWidth = 35;
export function DraggableEditorTabs({
hideLabels,
editorTabs,
editors,
currentTab,
onClickTab,
onCloseTab,
onCloseOtherTabs,
@@ -61,8 +57,6 @@ export function DraggableEditorTabs({
useOnResize(useForceUpdate());
const { windowSize } = useResponsiveWindowSize();
const currentTab = getCurrentTab(editorTabs);
React.useEffect(
() => {
if (!currentTab) return;
@@ -83,9 +77,8 @@ export function DraggableEditorTabs({
<ClosableTabs
hideLabels={hideLabels}
renderTabs={({ containerWidth }) => {
const editors = getEditors(editorTabs);
return editors.map((editorTab, id) => {
const isCurrentTab = getCurrentTabIndex(editorTabs) === id;
const isCurrentTab = currentTab === editorTab;
// Maximum width of a tab is the width so that all tabs can fit it,
// unless on a small screen, where we want to avoid compressing tabs too much

View File

@@ -53,8 +53,12 @@ export type EditorTab = {|
|};
export type EditorTabsState = {|
editors: Array<EditorTab>,
currentTab: number,
panes: {
[paneIdentifier: string]: {|
editors: Array<EditorTab>,
currentTab: number,
|},
},
|};
export type EditorKind =
@@ -82,6 +86,7 @@ export type EditorTabsPersistedState = {|
|};
export type EditorOpeningOptions = {|
paneIdentifier: string,
label?: string,
icon?: React.Node,
renderCustomIcon?: ?(brightness: number) => React.Node,
@@ -126,8 +131,16 @@ export const getEditorTabMetadata = (
export const getEditorTabsInitialState = (): EditorTabsState => {
return {
editors: [],
currentTab: 0,
panes: {
left: {
editors: [],
currentTab: 0,
},
center: {
editors: [],
currentTab: 0,
},
},
};
};
@@ -144,17 +157,28 @@ export const openEditorTab = (
extraEditorProps,
dontFocusTab,
closable,
paneIdentifier,
}: EditorOpeningOptions
): EditorTabsState => {
const existingEditorId = findIndex(
state.editors,
editor => editor.key === key
);
if (existingEditorId !== -1) {
return {
...state,
currentTab: dontFocusTab ? state.currentTab : existingEditorId,
};
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
const existingEditorId = findIndex(
pane.editors,
editor => editor.key === key
);
if (existingEditorId !== -1) {
return {
...state,
panes: {
...state.panes,
[paneIdentifier]: {
...pane,
currentTab: dontFocusTab ? pane.currentTab : existingEditorId,
},
},
};
}
}
const editorTab: EditorTab = {
@@ -170,46 +194,78 @@ export const openEditorTab = (
closable: typeof closable === 'undefined' ? true : !!closable,
};
const pane = state.panes[paneIdentifier];
if (!pane) {
throw new Error(`Pane with identifier "${paneIdentifier}" is not valid.`);
}
return {
...state,
editors:
// Make sure the home page is always the first tab.
key === 'start page'
? [editorTab, ...state.editors]
: [...state.editors, editorTab],
currentTab: dontFocusTab ? state.currentTab : state.editors.length,
panes: {
...state.panes,
[paneIdentifier]: {
...pane,
editors:
// Make sure the home page is always the first tab.
key === 'start page'
? [editorTab, ...pane.editors]
: [...pane.editors, editorTab],
currentTab: dontFocusTab ? pane.currentTab : pane.editors.length,
},
},
};
};
export const changeCurrentTab = (
state: EditorTabsState,
paneIdentifier: string,
newTabId: number
): EditorTabsState => {
const pane = state.panes[paneIdentifier];
if (!pane) {
throw new Error(`Pane with identifier "${paneIdentifier}" is not valid.`);
}
return {
...state,
currentTab: Math.max(0, Math.min(newTabId, state.editors.length - 1)),
panes: {
...state.panes,
[paneIdentifier]: {
...pane,
currentTab: Math.max(0, Math.min(newTabId, pane.editors.length - 1)),
},
},
};
};
export const isStartPageTabPresent = (state: EditorTabsState): boolean => {
return state.editors.some(editor => editor.key === 'start page');
return hasEditorTabOpenedWithKey(state, 'start page');
};
export const closeTabsExceptIf = (
state: EditorTabsState,
keepPredicate: (editorTab: EditorTab) => boolean
) => {
const currentEditorTab = getCurrentTab(state);
const remainingEditors = state.editors.filter(keepPredicate);
return changeCurrentTab(
{
...state,
editors: remainingEditors,
},
// Keep the focus on the current editor tab, or if it was closed
// go back to the first tab.
remainingEditors.indexOf(currentEditorTab) || 0
);
let newState = { ...state };
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
if (!pane) {
throw new Error(`Pane with identifier "${paneIdentifier}" is not valid.`);
}
const currentEditorTab = pane.editors[pane.currentTab] || null;
const paneRemainingEditors = pane.editors.filter(keepPredicate);
newState.panes[paneIdentifier] = {
...pane,
editors: pane.editors.filter(keepPredicate),
// Keep the focus on the current editor tab, or if it was closed
// go back to the first tab.
currentTab: paneRemainingEditors.indexOf(currentEditorTab) || 0,
};
}
return newState;
};
export const closeAllEditorTabs = (state: EditorTabsState): EditorTabsState => {
@@ -233,16 +289,27 @@ export const closeOtherEditorTabs = (
);
};
export const getEditors = (state: EditorTabsState): Array<EditorTab> => {
return state.editors;
export const getEditorsForPane = (
state: EditorTabsState,
paneIdentifier: string
): Array<EditorTab> => {
return state.panes[paneIdentifier].editors || [];
};
export const getCurrentTabIndex = (state: EditorTabsState): number => {
return state.currentTab;
export const getCurrentTabIndexForPane = (
state: EditorTabsState,
paneIdentifier: string
): number => {
const pane = state.panes[paneIdentifier];
return pane.currentTab || 0;
};
export const getCurrentTab = (state: EditorTabsState): EditorTab => {
return state.editors[state.currentTab];
export const getCurrentTabForPane = (
state: EditorTabsState,
paneIdentifier: string
): EditorTab | null => {
const pane = state.panes[paneIdentifier];
return pane.editors[pane.currentTab] || null;
};
export const closeProjectTabs = (
@@ -261,15 +328,22 @@ export const closeProjectTabs = (
* to the project.
*/
export const saveUiSettings = (state: EditorTabsState) => {
state.editors.forEach(editorTab => {
if (
editorTab.editorRef &&
(editorTab.editorRef instanceof SceneEditorContainer ||
editorTab.editorRef instanceof ExternalLayoutEditorContainer)
) {
editorTab.editorRef.saveUiSettings();
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
if (!pane) {
continue;
}
});
pane.editors.forEach(editorTab => {
if (
editorTab.editorRef &&
(editorTab.editorRef instanceof SceneEditorContainer ||
editorTab.editorRef instanceof ExternalLayoutEditorContainer)
) {
editorTab.editorRef.saveUiSettings();
}
});
}
};
/**
@@ -277,13 +351,20 @@ export const saveUiSettings = (state: EditorTabsState) => {
* to editors with changes to commit them (like modified extensions).
*/
export const notifyPreviewOrExportWillStart = (state: EditorTabsState) => {
state.editors.forEach(editorTab => {
const editor = editorTab.editorRef;
if (editor instanceof EventsFunctionsExtensionEditorContainer) {
editor.previewOrExportWillStart();
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
if (!pane) {
continue;
}
});
pane.editors.forEach(editorTab => {
const editor = editorTab.editorRef;
if (editor instanceof EventsFunctionsExtensionEditorContainer) {
editor.previewOrExportWillStart();
}
});
}
};
export const closeLayoutTabs = (state: EditorTabsState, layout: gdLayout) => {
@@ -405,14 +486,21 @@ export const closeEventsBasedObjectVariantTab = (
export const getEventsFunctionsExtensionEditor = (
state: EditorTabsState,
eventsFunctionsExtension: gdEventsFunctionsExtension
): ?{| editor: EventsFunctionsExtensionEditorContainer, tabIndex: number |} => {
for (let tabIndex = 0; tabIndex < state.editors.length; ++tabIndex) {
const editor = state.editors[tabIndex].editorRef;
if (
editor instanceof EventsFunctionsExtensionEditorContainer &&
editor.getEventsFunctionsExtension() === eventsFunctionsExtension
) {
return { editor, tabIndex };
): ?{|
editor: EventsFunctionsExtensionEditorContainer,
paneIdentifier: string,
tabIndex: number,
|} => {
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
for (let tabIndex = 0; tabIndex < pane.editors.length; ++tabIndex) {
const editor = pane.editors[tabIndex].editorRef;
if (
editor instanceof EventsFunctionsExtensionEditorContainer &&
editor.getEventsFunctionsExtension() === eventsFunctionsExtension
) {
return { editor, paneIdentifier, tabIndex };
}
}
}
@@ -424,16 +512,23 @@ export const getCustomObjectEditor = (
eventsFunctionsExtension: gdEventsFunctionsExtension,
eventsBasedObject: gdEventsBasedObject,
variantName: string
): ?{| editor: CustomObjectEditorContainer, tabIndex: number |} => {
for (let tabIndex = 0; tabIndex < state.editors.length; ++tabIndex) {
const editor = state.editors[tabIndex].editorRef;
if (
editor instanceof CustomObjectEditorContainer &&
editor.getEventsFunctionsExtension() === eventsFunctionsExtension &&
editor.getEventsBasedObject() === eventsBasedObject &&
editor.getVariantName() === variantName
) {
return { editor, tabIndex };
): ?{|
editor: CustomObjectEditorContainer,
paneIdentifier: string,
tabIndex: number,
|} => {
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
for (let tabIndex = 0; tabIndex < pane.editors.length; ++tabIndex) {
const editor = pane.editors[tabIndex].editorRef;
if (
editor instanceof CustomObjectEditorContainer &&
editor.getEventsFunctionsExtension() === eventsFunctionsExtension &&
editor.getEventsBasedObject() === eventsBasedObject &&
editor.getVariantName() === variantName
) {
return { editor, paneIdentifier, tabIndex };
}
}
}
@@ -442,6 +537,7 @@ export const getCustomObjectEditor = (
export const moveTabToTheRightOfHoveredTab = (
editorTabsState: EditorTabsState,
paneIdentifier: string,
movingTabIndex: number,
hoveredTabIndex: number
): EditorTabsState => {
@@ -450,21 +546,32 @@ export const moveTabToTheRightOfHoveredTab = (
const destinationIndex =
movingTabIndex > hoveredTabIndex ? hoveredTabIndex + 1 : hoveredTabIndex;
return moveTabToPosition(editorTabsState, movingTabIndex, destinationIndex);
return moveTabToPosition(
editorTabsState,
paneIdentifier,
movingTabIndex,
destinationIndex
);
};
export const moveTabToPosition = (
editorTabsState: EditorTabsState,
paneIdentifier: string,
fromIndex: number,
toIndex: number
): EditorTabsState => {
const currentEditorTabs = [...getEditors(editorTabsState)];
const movingTab = currentEditorTabs[fromIndex];
currentEditorTabs.splice(fromIndex, 1);
currentEditorTabs.splice(toIndex, 0, movingTab);
const paneNewEditorTabs = [
...getEditorsForPane(editorTabsState, paneIdentifier),
];
const movingTab = paneNewEditorTabs[fromIndex];
paneNewEditorTabs.splice(fromIndex, 1);
paneNewEditorTabs.splice(toIndex, 0, movingTab);
let currentTabIndex = getCurrentTabIndex(editorTabsState);
let currentTabNewIndex = currentTabIndex;
let currentTabIndex = getCurrentTabIndexForPane(
editorTabsState,
paneIdentifier
);
let paneNewCurrentTab = currentTabIndex;
const movingTabIsCurrentTab = fromIndex === currentTabIndex;
const tabIsMovedFromLeftToRightOfCurrentTab =
@@ -472,25 +579,65 @@ export const moveTabToPosition = (
const tabIsMovedFromRightToLeftOfCurrentTab =
fromIndex > currentTabIndex && toIndex <= currentTabIndex;
if (movingTabIsCurrentTab) currentTabNewIndex = toIndex;
else if (tabIsMovedFromLeftToRightOfCurrentTab) currentTabNewIndex -= 1;
else if (tabIsMovedFromRightToLeftOfCurrentTab) currentTabNewIndex += 1;
if (movingTabIsCurrentTab) paneNewCurrentTab = toIndex;
else if (tabIsMovedFromLeftToRightOfCurrentTab) paneNewCurrentTab -= 1;
else if (tabIsMovedFromRightToLeftOfCurrentTab) paneNewCurrentTab += 1;
return { editors: currentEditorTabs, currentTab: currentTabNewIndex };
return {
...editorTabsState,
panes: {
...editorTabsState.panes,
[paneIdentifier]: {
...editorTabsState.panes[paneIdentifier],
editors: paneNewEditorTabs,
currentTab: paneNewCurrentTab,
},
},
};
};
export const hasEditorTabOpenedWithKey = (
editorTabsState: EditorTabsState,
key: string
) => {
return !!editorTabsState.editors.find(editor => editor.key === key);
for (const paneIdentifier in editorTabsState.panes) {
const pane = editorTabsState.panes[paneIdentifier];
if (pane && pane.editors.find(editor => editor.key === key)) {
return true;
}
}
return false;
};
export const getOpenedAskAiEditor = (
state: EditorTabsState
): AskAiEditorInterface | null => {
const editor = state.editors.find(editor => editor.key === 'ask-ai');
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
const editor = pane && pane.editors.find(editor => editor.key === 'ask-ai');
if (editor) {
// $FlowFixMe - the key ensures that the editor is an AskAiEditorInterface.
return editor.editorRef || null;
}
}
// $FlowFixMe - the key ensures that the editor is an AskAiEditorInterface.
return (editor && editor.editorRef) || null;
return null;
};
export const getAllEditorTabs = (state: EditorTabsState): Array<EditorTab> => {
const allEditors = [];
for (const paneIdentifier in state.panes) {
const pane = state.panes[paneIdentifier];
allEditors.push(...pane.editors);
}
return allEditors;
};
export const hasEditorsInLeftPane = (state: EditorTabsState): boolean => {
if (!state.panes.left) {
return false;
}
return state.panes.left.editors.length > 0;
};

View File

@@ -76,11 +76,12 @@ const useEditorTabsStateSaving = ({
} = React.useContext(PreferencesContext);
const saveEditorState = React.useCallback(
() => {
// TODO: adapt for saving multiple panes.
// Do not save the state if the user is on the start page
if (!currentProjectId || editorTabs.currentTab === 0) return;
if (!currentProjectId || editorTabs.panes.center.currentTab === 0) return;
const editorState = {
currentTab: editorTabs.currentTab,
editors: editorTabs.editors
currentTab: editorTabs.panes.center.currentTab,
editors: editorTabs.panes.center.editors
.filter(editor => editor.key !== 'start page')
.map(getEditorTabMetadata),
};
@@ -174,9 +175,10 @@ const useEditorTabsStateSaving = ({
}
newEditorTabs = changeCurrentTab(
newEditorTabs,
'center',
shouldOpenSavedCurrentTab
? editorState.editorTabs.currentTab
: newEditorTabs.editors.length >= 1
: newEditorTabs.panes.center.editors.length >= 1
? 1
: 0
);

View File

@@ -0,0 +1,608 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
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 {
getEditorsForPane,
getCurrentTabIndexForPane,
getCurrentTabForPane,
type EditorTabsState,
type EditorTab,
hasEditorTabOpenedWithKey,
changeCurrentTab,
closeEditorTab,
closeOtherEditorTabs,
closeAllEditorTabs,
moveTabToTheRightOfHoveredTab,
saveUiSettings,
} from './EditorTabs/EditorTabsHandler';
import { type PreviewState } from './PreviewState';
import { type 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,
type FileMetadataAndStorageProviderName,
} from '../ProjectsStorage';
import UnsavedChangesContext from './UnsavedChangesContext';
import { type OpenedVersionStatus } from '../VersionHistory';
import { type StorageProvider } from '../ProjectsStorage';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { type PrivateGameTemplateListingData } from '../Utils/GDevelopServices/Shop';
import { type CourseChapter } from '../Utils/GDevelopServices/Asset';
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
import { type EventsFunctionsExtensionsState } from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
import { type ShareTab } from '../ExportAndShare/ShareDialog';
export type EditorTabsPaneCommonProps = {|
editorTabs: EditorTabsState,
currentProject: ?gdProject,
currentFileMetadata: ?FileMetadata,
tabsTitleBarAndEditorToolbarHidden: boolean,
setTabsTitleBarAndEditorToolbarHidden: (hidden: boolean) => void,
canSave: boolean,
isSavingProject: boolean,
isSharingEnabled: boolean,
hasPreviewsRunning: boolean,
previewState: PreviewState,
checkedOutVersionStatus: ?OpenedVersionStatus,
canDoNetworkPreview: boolean,
gamesPlatformFrameTools: GamesPlatformFrameTools,
// Callbacks from MainFrame
toggleProjectManager: () => void,
saveProject: () => Promise<void>,
openShareDialog: (tab?: ShareTab) => void,
launchDebuggerAndPreview: () => void,
launchNewPreview: (?{ numberOfWindows: number }) => Promise<void>,
launchNetworkPreview: () => Promise<void>,
launchHotReloadPreview: () => Promise<void>,
launchPreviewWithDiagnosticReport: () => Promise<void>,
setPreviewOverride: (override: {|
isPreviewOverriden: boolean,
overridenPreviewLayoutName: ?string,
overridenPreviewExternalLayoutName: ?string,
|}) => void,
openVersionHistoryPanel: () => void,
onQuitVersionHistory: () => Promise<void>,
openAskAi: () => void,
getStorageProvider: () => StorageProvider,
setPreviewedLayout: (layoutName: ?string) => void,
openExternalEvents: (name: string) => void,
openLayout: (
name: string,
options?: {|
openEventsEditor: boolean,
openSceneEditor: boolean,
focusWhenOpened:
| 'scene-or-events-otherwise'
| 'scene'
| 'events'
| 'none',
|}
) => void,
openTemplateFromTutorial: (tutorialId: string) => Promise<void>,
openTemplateFromCourseChapter: (
courseChapter: CourseChapter,
templateId?: string
) => Promise<void>,
previewDebuggerServer: ?any,
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
resourceManagementProps: ResourceManagementProps,
onCreateEventsFunction: (
extensionName: string,
eventsFunction: gdEventsFunction,
editorIdentifier:
| 'scene-events-editor'
| 'extension-events-editor'
| 'external-events-editor'
) => void,
openInstructionOrExpression: (
extension: gdPlatformExtension,
type: string
) => void,
onOpenCustomObjectEditor: (
eventsFunctionsExtension: gdEventsFunctionsExtension,
eventsBasedObject: gdEventsBasedObject,
variantName: string
) => void,
onRenamedEventsBasedObject: (
eventsFunctionsExtension: gdEventsFunctionsExtension,
oldName: string,
newName: string
) => void,
onDeletedEventsBasedObject: (
eventsFunctionsExtension: gdEventsFunctionsExtension,
name: string
) => void,
openObjectEvents: (extensionName: string, objectName: string) => void,
canOpen: boolean,
openOpenFromStorageProviderDialog: () => void,
openFromFileMetadataWithStorageProvider: (
file: FileMetadataAndStorageProviderName
) => Promise<void>,
openNewProjectDialog: () => void,
openProjectManager: (open: boolean) => void,
askToCloseProject: () => Promise<boolean>,
closeProject: () => Promise<void>,
onSelectExampleShortHeader: ({|
exampleShortHeader: ?ExampleShortHeader,
preventBackHome?: boolean,
|}) => void,
onSelectPrivateGameTemplateListingData: ({|
privateGameTemplateListingData: ?PrivateGameTemplateListingData,
preventBackHome?: boolean,
|}) => void,
createEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
createProjectFromExample: (
exampleShortHeader: ExampleShortHeader,
newProjectSetup: NewProjectSetup,
i18n: I18nType,
isQuickCustomization?: boolean
) => Promise<void>,
onOpenProfileDialog: () => void,
openLanguageDialog: (open: boolean) => void,
openPreferencesDialog: (open: boolean) => void,
openAboutDialog: (open: boolean) => void,
selectInAppTutorial: (tutorialId: string) => void,
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
isProjectClosedSoAvoidReloadingExtensions: boolean,
renameResourcesInProject: (
project: gdProject,
renames: { [string]: string }
) => void,
openBehaviorEvents: (extensionName: string, behaviorName: string) => void,
onExtractAsExternalLayout: (name: string) => void,
onOpenEventBasedObjectEditor: (
extensionName: string,
eventsBasedObjectName: string
) => void,
onOpenEventBasedObjectVariantEditor: (
extensionName: string,
eventsBasedObjectName: string,
variantName: string
) => void,
deleteEventsBasedObjectVariant: (
eventsFunctionsExtension: gdEventsFunctionsExtension,
eventsBasedObject: gdEventsBasedObject,
variant: gdEventsBasedObjectVariant
) => void,
onEventsBasedObjectChildrenEdited: (
eventsBasedObject: gdEventsBasedObject
) => void,
onSceneObjectEdited: (
scene: gdLayout,
objectWithContext: ObjectWithContext
) => void,
onSceneObjectsDeleted: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
gamesList: GamesList,
setEditorTabs: (editorTabs: EditorTabsState) => void,
|};
type Props = {|
...EditorTabsPaneCommonProps,
paneIdentifier: string,
isLeftMost: boolean,
isRightMost: boolean,
|};
const EditorTabsPane = React.forwardRef<Props, {||}>((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,
setEditorTabs,
paneIdentifier,
isLeftMost,
isRightMost,
} = 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: ?React.Node, isCurrentTab: boolean = true) => {
if (!toolbarRef.current || !isCurrentTab) return;
toolbarRef.current.setEditorToolbar(editorToolbar || null);
},
[]
);
const updateToolbar = React.useCallback(
() => {
const editorTab = getCurrentTabForPane(editorTabs, paneIdentifier);
if (!editorTab || !editorTab.editorRef) {
setEditorToolbar(null);
return;
}
editorTab.editorRef.updateToolbar();
},
[editorTabs, setEditorToolbar, paneIdentifier]
);
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, paneIdentifier, value);
setEditorTabs(newEditorTabs);
const newCurrentTab = getCurrentTabForPane(newEditorTabs, paneIdentifier);
if (newCurrentTab) {
_onEditorTabActivated(newCurrentTab);
}
},
[editorTabs, setEditorTabs, _onEditorTabActivated, paneIdentifier]
);
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,
paneIdentifier,
fromIndex,
toHoveredIndex
)
);
},
[editorTabs, paneIdentifier, setEditorTabs]
);
const paneEditorTabs = getEditorsForPane(editorTabs, paneIdentifier);
const currentTab = getCurrentTabForPane(editorTabs, paneIdentifier);
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0,
minWidth: 0,
}}
>
<TabsTitlebar
isLeftMost={isLeftMost}
isRightMost={isRightMost}
hidden={tabsTitleBarAndEditorToolbarHidden}
toggleProjectManager={toggleProjectManager}
renderTabs={(onEditorTabHovered, onEditorTabClosing) => (
<DraggableEditorTabs
hideLabels={false}
editors={paneEditorTabs}
currentTab={currentTab}
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);
}}
onCloseOtherTabs={(editorTab: EditorTab) => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseOtherEditorTabs(editorTab);
}}
onCloseAll={() => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseAllEditorTabs();
}}
onTabActivated={(editorTab: EditorTab) =>
_onEditorTabActivated(editorTab)
}
onDropTab={onDropEditorTab}
onHoverTab={(
editorTab: ?EditorTab,
options: {| isLabelTruncated: boolean |}
) => onEditorTabHovered(editorTab, options)}
/>
)}
hasAskAiOpened={hasAskAiOpened}
onOpenAskAi={openAskAi}
/>
<Toolbar
ref={toolbarRef}
hidden={tabsTitleBarAndEditorToolbarHidden}
showProjectButtons={
!['start page', 'debugger', 'ask-ai', null].includes(
currentTab ? currentTab.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}
/>
{paneEditorTabs.map((editorTab, id) => {
const isCurrentTab =
getCurrentTabIndexForPane(editorTabs, paneIdentifier) === 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>
);
})}
</div>
);
});
export default EditorTabsPane;

View File

@@ -0,0 +1,36 @@
.container {
display: flex;
width: 100%;
height: 100%;
}
.pane {
display: flex;
flex-direction: column;
overflow: auto;
}
.leftPane {
flex-basis: 300px;
flex-shrink: 0;
}
.centerPane {
flex: 1;
}
.hidden {
display: none;
}
.resizer {
width: 2px;
cursor: ew-resize;
background-color: #d9d9de;
flex-shrink: 0;
}
.resizer:hover,
.resizer:focus {
background-color: #999;
outline: none;
}

View File

@@ -0,0 +1,125 @@
// @flow
import * as React from 'react';
import classes from './PanesContainer.module.css';
import classNames from 'classnames';
type Props = {|
renderPane: ({
paneIdentifier: string,
isLeftMost: boolean,
isRightMost: boolean,
}) => React.Node,
isLeftPaneOpened: boolean,
|};
type DraggingState = {|
startClientX: number,
startWidth: number,
|};
const paneWidthMin = 100;
export const PanesContainer = ({ renderPane, isLeftPaneOpened }: Props) => {
const containerRef = React.useRef<HTMLDivElement | null>(null);
const resizerRef = React.useRef<HTMLDivElement | null>(null);
const leftPaneRef = React.useRef<HTMLDivElement | null>(null);
const draggingStateRef = React.useRef<DraggingState | null>(null);
React.useEffect(() => {
const onPointerMove = (event: PointerEvent) => {
const leftPane = leftPaneRef.current;
const draggingState = draggingStateRef.current;
if (!draggingState || !containerRef.current || !leftPane) return;
const containerRect = containerRef.current.getBoundingClientRect();
const newLeftWidth =
draggingState.startWidth + (event.clientX - draggingState.startClientX);
const min = paneWidthMin;
const max = containerRect.width - paneWidthMin;
const clampedWidth = Math.max(min, Math.min(max, newLeftWidth));
leftPane.style.flexBasis = `${clampedWidth}px`;
};
const onPointerUp = () => {
draggingStateRef.current = null;
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
};
const onPointerDown = (event: PointerEvent) => {
event.preventDefault();
const leftPane = leftPaneRef.current;
if (!leftPane) return;
draggingStateRef.current = {
startClientX: event.clientX,
startWidth: leftPane.getBoundingClientRect().width,
};
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
};
const resizer = resizerRef.current;
if (resizer) {
resizer.addEventListener('pointerdown', onPointerDown);
}
return () => {
if (resizer) {
resizer.removeEventListener('pointerdown', onPointerDown);
}
};
}, []);
return (
<div
ref={containerRef}
className={classes.container}
role="group"
aria-label="Resizable split pane"
>
<div
ref={leftPaneRef}
className={classNames({
[classes.pane]: true,
[classes.leftPane]: true,
[classes.hidden]: !isLeftPaneOpened,
})}
id="pane-left"
>
{renderPane({
paneIdentifier: 'left',
isLeftMost: true,
isRightMost: false,
})}
</div>
<div
className={classNames({
[classes.resizer]: true,
[classes.hidden]: !isLeftPaneOpened,
})}
role="separator"
aria-orientation="vertical"
aria-controls="pane-left pane-right"
tabIndex={0}
ref={resizerRef}
/>
<div
className={classNames({
[classes.pane]: true,
[classes.centerPane]: true,
})}
id="pane-right"
>
{renderPane({
paneIdentifier: 'center',
isLeftMost: !isLeftPaneOpened,
isRightMost: true,
})}
</div>
</div>
);
};

View File

@@ -54,6 +54,8 @@ type TabsTitlebarProps = {|
) => React.Node,
hasAskAiOpened: boolean,
onOpenAskAi: () => void,
isLeftMost: boolean,
isRightMost: boolean,
|};
const useIsAskAiIconAnimated = (shouldDisplayAskAi: boolean) => {
@@ -105,6 +107,8 @@ export default function TabsTitlebar({
renderTabs,
hasAskAiOpened,
onOpenAskAi,
isLeftMost,
isRightMost,
}: TabsTitlebarProps) {
const isTouchscreen = useScreenType() === 'touch';
const gdevelopTheme = React.useContext(GDevelopThemeContext);
@@ -191,6 +195,7 @@ export default function TabsTitlebar({
const shouldDisplayAskAi =
preferences.values.showAiAskButtonInTitleBar &&
!hasAskAiOpened &&
isRightMost &&
!hideAskAi;
const isAskAiIconAnimated = useIsAskAiIconAnimated(shouldDisplayAskAi);
@@ -204,20 +209,22 @@ export default function TabsTitlebar({
}}
className={WINDOW_DRAGGABLE_PART_CLASS_NAME}
>
<TitleBarLeftSafeMargins />
<IconButton
size="small"
// Even if not in the toolbar, keep this ID for backward compatibility for tutorials.
id="main-toolbar-project-manager-button"
// The whole bar is draggable, so prevent the icon to be draggable,
// as it can affect the ability to open the menu.
className={WINDOW_NON_DRAGGABLE_PART_CLASS_NAME}
style={styles.menuIcon}
color="default"
onClick={toggleProjectManager}
>
<MenuIcon />
</IconButton>
{isLeftMost && <TitleBarLeftSafeMargins />}
{isLeftMost && (
<IconButton
size="small"
// Even if not in the toolbar, keep this ID for backward compatibility for tutorials.
id="main-toolbar-project-manager-button"
// The whole bar is draggable, so prevent the icon to be draggable,
// as it can affect the ability to open the menu.
className={WINDOW_NON_DRAGGABLE_PART_CLASS_NAME}
style={styles.menuIcon}
color="default"
onClick={toggleProjectManager}
>
<MenuIcon />
</IconButton>
)}
{renderTabs(onEditorTabHovered, onEditorTabClosing)}
{shouldDisplayAskAi ? (
<div
@@ -231,7 +238,7 @@ export default function TabsTitlebar({
/>
</div>
) : null}
<TitleBarRightSafeMargins />
{isRightMost && <TitleBarRightSafeMargins />}
{tooltipData && (
<TabsTitlebarTooltip
anchorElement={tooltipData.element}

View File

@@ -11,7 +11,6 @@ import EventsIcon from '../UI/CustomSvgIcons/Events';
import ExternalEventsIcon from '../UI/CustomSvgIcons/ExternalEvents';
import ExternalLayoutIcon from '../UI/CustomSvgIcons/ExternalLayout';
import ExtensionIcon from '../UI/CustomSvgIcons/Extension';
import Toolbar, { type ToolbarInterface } from './Toolbar';
import ProjectTitlebar from './ProjectTitlebar';
import PreferencesDialog from './Preferences/PreferencesDialog';
import AboutDialog from './AboutDialog';
@@ -21,18 +20,12 @@ import CloseConfirmDialog from '../UI/CloseConfirmDialog';
import ProfileDialog from '../Profile/ProfileDialog';
import Window from '../Utils/Window';
import { showErrorBox } from '../UI/Messages/MessageBox';
import { TabContentContainer } from '../UI/ClosableTabs';
import { DraggableEditorTabs } from './EditorTabs/DraggableEditorTabs';
import EditorTabsPane, {
type EditorTabsPaneCommonProps,
} from './EditorTabsPane';
import {
getEditorTabsInitialState,
openEditorTab,
closeEditorTab,
closeOtherEditorTabs,
closeAllEditorTabs,
changeCurrentTab,
getEditors,
getCurrentTabIndex,
getCurrentTab,
closeProjectTabs,
closeLayoutTabs,
closeExternalLayoutTabs,
@@ -42,14 +35,14 @@ import {
closeEventsBasedObjectVariantTab,
saveUiSettings,
type EditorTabsState,
type EditorTab,
type EditorKind,
getEventsFunctionsExtensionEditor,
notifyPreviewOrExportWillStart,
moveTabToTheRightOfHoveredTab,
getCustomObjectEditor,
hasEditorTabOpenedWithKey,
getOpenedAskAiEditor,
changeCurrentTab,
getAllEditorTabs,
hasEditorsInLeftPane,
} from './EditorTabs/EditorTabsHandler';
import { renderDebuggerEditorContainer } from './EditorContainers/DebuggerEditorContainer';
import { renderEventsEditorContainer } from './EditorContainers/EventsEditorContainer';
@@ -65,9 +58,6 @@ import {
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
} from './EditorContainers/BaseEditor';
import ErrorBoundary, {
getEditorErrorBoundaryProps,
} from '../UI/ErrorBoundary';
import { type Exporter } from '../ExportAndShare/ShareDialog';
import ResourcesLoader from '../ResourcesLoader/index';
import {
@@ -132,7 +122,6 @@ import {
CommandPaletteWithAlgoliaSearch,
type CommandPaletteInterface,
} from '../CommandPalette/CommandPalette';
import CommandsContextScopedProvider from '../CommandPalette/CommandsScopedContext';
import { isExtensionNameTaken } from '../ProjectManager/EventFunctionExtensionNameVerifier';
import {
type PreviewState,
@@ -174,7 +163,6 @@ 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,
@@ -208,6 +196,7 @@ import RobotIcon from '../ProjectCreation/RobotIcon';
import PublicProfileContext from '../Profile/PublicProfileContext';
import { useGamesPlatformFrame } from './EditorContainers/HomePage/PlaySection/UseGamesPlatformFrame';
import { useExtensionLoadErrorDialog } from '../Utils/UseExtensionLoadErrorDialog';
import { PanesContainer } from './PanesContainer';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
@@ -354,7 +343,6 @@ const MainFrame = (props: Props) => {
gdjsDevelopmentWatcherEnabled: false,
}: State)
);
const toolbar = React.useRef<?ToolbarInterface>(null);
const [
tabsTitleBarAndEditorToolbarHidden,
setTabsTitleBarAndEditorToolbarHidden,
@@ -680,6 +668,7 @@ const MainFrame = (props: Props) => {
extraEditorProps,
key,
dontFocusTab,
paneIdentifier: kind === 'ask-ai' ? 'left' : 'center',
};
},
[i18n, props.storageProviders]
@@ -773,26 +762,6 @@ 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
// on all React components, as it's handled by GDI18nProvider.
@@ -1284,18 +1253,11 @@ const MainFrame = (props: Props) => {
const toggleProjectManager = React.useCallback(
() => {
if (toolbar.current)
openProjectManager(projectManagerOpen => !projectManagerOpen);
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;
@@ -2168,7 +2130,11 @@ const MainFrame = (props: Props) => {
);
setState(state => ({
...state,
editorTabs: changeCurrentTab(editorTabs, foundTab.tabIndex),
editorTabs: changeCurrentTab(
editorTabs,
foundTab.paneIdentifier,
foundTab.tabIndex
),
}));
} else {
// Open a new editor for the extension and the given function
@@ -2205,7 +2171,11 @@ const MainFrame = (props: Props) => {
if (foundTab) {
setState(state => ({
...state,
editorTabs: changeCurrentTab(editorTabs, foundTab.tabIndex),
editorTabs: changeCurrentTab(
editorTabs,
foundTab.paneIdentifier,
foundTab.tabIndex
),
}));
} else {
// Open a new editor for the extension and the given function
@@ -2249,7 +2219,11 @@ const MainFrame = (props: Props) => {
foundTab.editor.selectEventsBasedBehaviorByName(objectName);
setState(state => ({
...state,
editorTabs: changeCurrentTab(editorTabs, foundTab.tabIndex),
editorTabs: changeCurrentTab(
editorTabs,
foundTab.paneIdentifier,
foundTab.tabIndex
),
}));
} else {
// Open a new editor for the extension and the given function
@@ -2282,7 +2256,11 @@ const MainFrame = (props: Props) => {
foundTab.editor.selectEventsBasedBehaviorByName(behaviorName);
setState(state => ({
...state,
editorTabs: changeCurrentTab(editorTabs, foundTab.tabIndex),
editorTabs: changeCurrentTab(
editorTabs,
foundTab.paneIdentifier,
foundTab.tabIndex
),
}));
} else {
// Open a new editor for the extension and the given function
@@ -2374,7 +2352,7 @@ const MainFrame = (props: Props) => {
eventsBasedObject
);
for (const editor of state.editorTabs.editors) {
for (const editor of getAllEditorTabs(state.editorTabs)) {
const { editorRef } = editor;
if (editorRef) {
editorRef.onEventsBasedObjectChildrenEdited();
@@ -2386,7 +2364,7 @@ const MainFrame = (props: Props) => {
const onSceneObjectEdited = React.useCallback(
(scene: gdLayout, objectWithContext: ObjectWithContext) => {
for (const editor of state.editorTabs.editors) {
for (const editor of getAllEditorTabs(state.editorTabs)) {
const { editorRef } = editor;
if (editorRef) {
editorRef.onSceneObjectEdited(scene, objectWithContext);
@@ -2398,7 +2376,7 @@ const MainFrame = (props: Props) => {
const onSceneObjectsDeleted = React.useCallback(
(scene: gdLayout) => {
for (const editor of state.editorTabs.editors) {
for (const editor of getAllEditorTabs(state.editorTabs)) {
const { editorRef } = editor;
if (editorRef) {
editorRef.onSceneObjectsDeleted(scene);
@@ -2410,7 +2388,7 @@ const MainFrame = (props: Props) => {
const onSceneEventsModifiedOutsideEditor = React.useCallback(
(changes: SceneEventsOutsideEditorChanges) => {
for (const editor of state.editorTabs.editors) {
for (const editor of getAllEditorTabs(state.editorTabs)) {
const { editorRef } = editor;
if (editorRef) {
editorRef.onSceneEventsModifiedOutsideEditor(changes);
@@ -3249,62 +3227,6 @@ 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) => {
if (shouldCloseProject) {
@@ -3833,7 +3755,87 @@ const MainFrame = (props: Props) => {
!!state.currentProject &&
!isSavingProject &&
(!currentFileMetadata || !isProjectOwnedBySomeoneElse);
const hasAskAiOpened = hasEditorTabOpenedWithKey(state.editorTabs, 'ask-ai');
const editorTabsPaneProps: EditorTabsPaneCommonProps = {
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,
};
const isLeftPaneOpened = hasEditorsInLeftPane(state.editorTabs);
return (
<div
@@ -3905,80 +3907,6 @@ const MainFrame = (props: Props) => {
buildMainMenuProps={buildMainMenuProps}
/>
</ProjectManagerDrawer>
<TabsTitlebar
hidden={tabsTitleBarAndEditorToolbarHidden}
toggleProjectManager={toggleProjectManager}
renderTabs={(onEditorTabHovered, onEditorTabClosing) => (
<DraggableEditorTabs
hideLabels={false}
editorTabs={state.editorTabs}
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);
}}
onCloseOtherTabs={(editorTab: EditorTab) => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseOtherEditorTabs(editorTab);
}}
onCloseAll={() => {
// Call onEditorTabClosing before to ensure any tooltip is removed before the tab is closed.
onEditorTabClosing();
_onCloseAllEditorTabs();
}}
onTabActivated={(editorTab: EditorTab) =>
_onEditorTabActivated(editorTab)
}
onDropTab={onDropEditorTab}
onHoverTab={(
editorTab: ?EditorTab,
options: {| isLabelTruncated: boolean |}
) => onEditorTabHovered(editorTab, options)}
/>
)}
hasAskAiOpened={hasAskAiOpened}
onOpenAskAi={openAskAi}
/>
<Toolbar
ref={toolbar}
hidden={tabsTitleBarAndEditorToolbarHidden}
showProjectButtons={
!['start page', 'debugger', 'ask-ai', null].includes(
getCurrentTab(state.editorTabs)
? getCurrentTab(state.editorTabs).key
: null
)
}
canSave={canSave}
onSave={saveProject}
openShareDialog={() =>
openShareDialog(/* leave the dialog decide which tab to open */)
}
isSharingEnabled={
!checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId
}
onOpenDebugger={launchDebuggerAndPreview}
hasPreviewsRunning={hasPreviewsRunning}
onPreviewWithoutHotReload={launchNewPreview}
onNetworkPreview={launchNetworkPreview}
onHotReloadPreview={launchHotReloadPreview}
onLaunchPreviewWithDiagnosticReport={launchPreviewWithDiagnosticReport}
canDoNetworkPreview={
!!_previewLauncher.current &&
_previewLauncher.current.canDoNetworkPreview()
}
setPreviewOverride={setPreviewOverride}
isPreviewEnabled={
!!currentProject && currentProject.getLayoutsCount() > 0
}
previewState={previewState}
onOpenVersionHistory={openVersionHistoryPanel}
checkedOutVersionStatus={checkedOutVersionStatus}
onQuitVersionHistory={onQuitVersionHistory}
canQuitVersionHistory={!isSavingProject}
/>
{// Render games platform frame before the editors, so the editor have priority
// in what to display (ex: Loader of play section)
gamesPlatformFrameTools.renderGamesPlatformFrame()}
@@ -3987,148 +3915,17 @@ const MainFrame = (props: Props) => {
state.currentProject ? state.currentProject.getProjectUuid() : ''
}
>
{getEditors(state.editorTabs).map((editorTab, id) => {
const isCurrentTab = getCurrentTabIndex(state.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: 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
) => {
// 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>
);
})}
<PanesContainer
isLeftPaneOpened={isLeftPaneOpened}
renderPane={({ paneIdentifier, isLeftMost, isRightMost }) => (
<EditorTabsPane
{...editorTabsPaneProps}
paneIdentifier={paneIdentifier}
isLeftMost={isLeftMost}
isRightMost={isRightMost}
/>
)}
/>
</LeaderboardProvider>
<CommandPaletteWithAlgoliaSearch ref={commandPaletteRef} />
<LoaderModal