mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add crude working version of panes
This commit is contained in:
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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
|
||||
);
|
||||
|
608
newIDE/app/src/MainFrame/EditorTabsPane.js
Normal file
608
newIDE/app/src/MainFrame/EditorTabsPane.js
Normal 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;
|
@@ -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;
|
||||
}
|
125
newIDE/app/src/MainFrame/PanesContainer/index.js
Normal file
125
newIDE/app/src/MainFrame/PanesContainer/index.js
Normal 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>
|
||||
);
|
||||
};
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user