mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Display "Ask AI" as a separate pane (or drawer on small screens) (#7738)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ import { ChatBubble } from './ChatBubble';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { ChatMarkdownText } from './ChatMarkdownText';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import { getFunctionCallToFunctionCallOutputMap } from './AiRequestUtils';
|
||||
import { getFunctionCallToFunctionCallOutputMap } from '../AiRequestUtils';
|
||||
import { FunctionCallRow } from './FunctionCallRow';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Like from '../../UI/CustomSvgIcons/Like';
|
||||
|
@@ -29,7 +29,7 @@ import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import Window from '../../Utils/Window';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
import { type EditorCallbacks } from '../../EditorFunctions';
|
||||
import { getFunctionCallsToProcess } from './AiRequestUtils';
|
||||
import { getFunctionCallsToProcess } from '../AiRequestUtils';
|
||||
import CircularProgress from '../../UI/CircularProgress';
|
||||
import TwoStatesButton from '../../UI/TwoStatesButton';
|
||||
import Help from '../../UI/CustomSvgIcons/Help';
|
||||
@@ -68,6 +68,7 @@ type Props = {
|
||||
|}) => void,
|
||||
onSendMessage: (options: {|
|
||||
userMessage: string,
|
||||
createdSceneNames?: Array<string>,
|
||||
|}) => Promise<void>,
|
||||
onSendFeedback: (
|
||||
aiRequestId: string,
|
||||
@@ -79,7 +80,10 @@ type Props = {
|
||||
hasOpenedProject: boolean,
|
||||
isAutoProcessingFunctionCalls: boolean,
|
||||
setAutoProcessFunctionCalls: boolean => void,
|
||||
onStartNewChat: (mode: 'chat' | 'agent') => void,
|
||||
onStartOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
initialMode?: 'chat' | 'agent',
|
||||
|
||||
onProcessFunctionCalls: (
|
||||
@@ -123,12 +127,17 @@ const getQuotaOrCreditsText = ({
|
||||
<>
|
||||
{increaseQuotaOffering === 'subscribe' ? (
|
||||
<Trans>
|
||||
Get GDevelop premium to get more free requests every month.
|
||||
Get GDevelop premium to get more free requests every day.
|
||||
</Trans>
|
||||
) : quota.period === '30days' ? (
|
||||
<Trans>
|
||||
These are parts of your GDevelop premium membership ({quota.max}{' '}
|
||||
free requests per month).
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
These are parts of your GDevelop premium membership ({quota.max}{' '}
|
||||
free requests per month).
|
||||
free requests per day).
|
||||
</Trans>
|
||||
)}{' '}
|
||||
<Trans>Free requests do not consume credits on your account.</Trans>
|
||||
@@ -248,7 +257,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
onStartNewAiRequest,
|
||||
onSendMessage,
|
||||
onSendFeedback,
|
||||
onStartNewChat,
|
||||
onStartOrOpenChat,
|
||||
initialMode,
|
||||
quota,
|
||||
increaseQuotaOffering,
|
||||
@@ -426,7 +435,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
<RobotIcon rotating size={40} />
|
||||
</Line>
|
||||
<Column noMargin alignItems="center">
|
||||
<Text size="bold-title">
|
||||
<Text size="bold-title" align="center">
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Trans>What do you want to make?</Trans>
|
||||
) : (
|
||||
@@ -439,7 +448,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
value={newAiRequestMode}
|
||||
leftButton={{
|
||||
icon: <Hammer fontSize="small" />,
|
||||
label: <Trans>Build for me (beta)</Trans>,
|
||||
label: <Trans>Build for me</Trans>,
|
||||
value: 'agent',
|
||||
}}
|
||||
rightButton={{
|
||||
@@ -546,6 +555,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
</>
|
||||
) : null}
|
||||
</ColumnStackLayout>
|
||||
<Spacer />
|
||||
<Column justifyContent="center">
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
@@ -647,7 +657,12 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
This request is for another project.{' '}
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => onStartNewChat(aiRequest.mode || 'chat')}
|
||||
onClick={() =>
|
||||
onStartOrOpenChat({
|
||||
mode: aiRequest.mode || 'chat',
|
||||
aiRequestId: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
Start a new chat
|
||||
</Link>{' '}
|
||||
|
229
newIDE/app/src/AiGeneration/AiRequestContext.js
Normal file
229
newIDE/app/src/AiGeneration/AiRequestContext.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
getAiRequest,
|
||||
type AiRequest,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { type EditorFunctionCallResult } from '../EditorFunctions/EditorFunctionCallRunner';
|
||||
|
||||
type EditorFunctionCallResultsStorage = {|
|
||||
getEditorFunctionCallResults: (
|
||||
aiRequestId: string
|
||||
) => Array<EditorFunctionCallResult> | null,
|
||||
addEditorFunctionCallResults: (
|
||||
aiRequestId: string,
|
||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||
) => void,
|
||||
clearEditorFunctionCallResults: (aiRequestId: string) => void,
|
||||
|};
|
||||
|
||||
const useEditorFunctionCallResultsStorage = (): EditorFunctionCallResultsStorage => {
|
||||
const [
|
||||
editorFunctionCallResultsPerRequest,
|
||||
setEditorFunctionCallResultsPerRequest,
|
||||
] = React.useState<{
|
||||
[aiRequestId: string]: Array<EditorFunctionCallResult>,
|
||||
}>({});
|
||||
|
||||
return {
|
||||
getEditorFunctionCallResults: React.useCallback(
|
||||
(aiRequestId: string): Array<EditorFunctionCallResult> | null =>
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || null,
|
||||
[editorFunctionCallResultsPerRequest]
|
||||
),
|
||||
addEditorFunctionCallResults: React.useCallback(
|
||||
(
|
||||
aiRequestId: string,
|
||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||
) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => {
|
||||
const existingEditorFunctionCallResults = (
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || []
|
||||
).filter(existingEditorFunctionCallResult => {
|
||||
return !editorFunctionCallResults.some(
|
||||
editorFunctionCallResult => {
|
||||
return (
|
||||
editorFunctionCallResult.call_id ===
|
||||
existingEditorFunctionCallResult.call_id
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: [
|
||||
...existingEditorFunctionCallResults,
|
||||
...editorFunctionCallResults,
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
[]
|
||||
),
|
||||
clearEditorFunctionCallResults: React.useCallback((aiRequestId: string) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => ({
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: null,
|
||||
})
|
||||
);
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
|
||||
type AiRequestStorage = {|
|
||||
aiRequests: { [string]: AiRequest },
|
||||
updateAiRequest: (aiRequestId: string, aiRequest: AiRequest) => void,
|
||||
refreshAiRequest: (aiRequestId: string) => Promise<void>,
|
||||
isSendingAiRequest: (aiRequestId: string | null) => boolean,
|
||||
getLastSendError: (aiRequestId: string | null) => ?Error,
|
||||
setSendingAiRequest: (aiRequestId: string | null, isSending: boolean) => void,
|
||||
setLastSendError: (aiRequestId: string | null, lastSendError: ?Error) => void,
|
||||
|};
|
||||
|
||||
type AiRequestSendState = {|
|
||||
isSending: boolean,
|
||||
lastSendError: ?Error,
|
||||
|};
|
||||
|
||||
export const useAiRequestsStorage = (): AiRequestStorage => {
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
|
||||
const [aiRequests, setAiRequests] = React.useState<{ [string]: AiRequest }>(
|
||||
{}
|
||||
);
|
||||
|
||||
const updateAiRequest = React.useCallback(
|
||||
(aiRequestId: string, aiRequest: AiRequest) => {
|
||||
setAiRequests(aiRequests => ({
|
||||
...aiRequests,
|
||||
[aiRequestId]: aiRequest,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const refreshAiRequest = React.useCallback(
|
||||
async (aiRequestId: string) => {
|
||||
if (!profile) return;
|
||||
|
||||
try {
|
||||
const updatedAiRequest = await getAiRequest(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
aiRequestId: aiRequestId,
|
||||
});
|
||||
updateAiRequest(updatedAiRequest.id, updatedAiRequest);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error while background refreshing AI request - ignoring:',
|
||||
error
|
||||
);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, profile, updateAiRequest]
|
||||
);
|
||||
|
||||
const [aiRequestSendStates, setAiRequestSendStates] = React.useState<{
|
||||
[string]: AiRequestSendState,
|
||||
}>({});
|
||||
const isSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
!!aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].isSending,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const getLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
(aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].lastSendError) ||
|
||||
null,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const setSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null, isSending: boolean) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending,
|
||||
lastSendError: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
const setLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null, lastSendError: ?Error) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending: false,
|
||||
lastSendError,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
|
||||
return {
|
||||
aiRequests,
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
setSendingAiRequest,
|
||||
setLastSendError,
|
||||
getLastSendError,
|
||||
};
|
||||
};
|
||||
|
||||
type AiRequestContextState = {|
|
||||
aiRequestStorage: AiRequestStorage,
|
||||
editorFunctionCallResultsStorage: EditorFunctionCallResultsStorage,
|
||||
|};
|
||||
|
||||
export const AiRequestContext = React.createContext<AiRequestContextState>({
|
||||
aiRequestStorage: {
|
||||
aiRequests: {},
|
||||
updateAiRequest: () => {},
|
||||
refreshAiRequest: async () => {},
|
||||
isSendingAiRequest: () => false,
|
||||
getLastSendError: () => null,
|
||||
setSendingAiRequest: () => {},
|
||||
setLastSendError: () => {},
|
||||
},
|
||||
editorFunctionCallResultsStorage: {
|
||||
getEditorFunctionCallResults: () => [],
|
||||
addEditorFunctionCallResults: () => {},
|
||||
clearEditorFunctionCallResults: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
type AiRequestProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
export const AiRequestProvider = ({ children }: AiRequestProviderProps) => {
|
||||
const editorFunctionCallResultsStorage = useEditorFunctionCallResultsStorage();
|
||||
const aiRequestStorage = useAiRequestsStorage();
|
||||
|
||||
const state = React.useMemo(
|
||||
() => ({
|
||||
aiRequestStorage,
|
||||
editorFunctionCallResultsStorage,
|
||||
}),
|
||||
[aiRequestStorage, editorFunctionCallResultsStorage]
|
||||
);
|
||||
|
||||
return (
|
||||
<AiRequestContext.Provider value={state}>
|
||||
{children}
|
||||
</AiRequestContext.Provider>
|
||||
);
|
||||
};
|
@@ -3,8 +3,8 @@ import {
|
||||
type AiRequest,
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
type AiRequestFunctionCallOutput,
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import { type EditorFunctionCallResult } from '../EditorFunctions/EditorFunctionCallRunner';
|
||||
|
||||
export const getFunctionCallToFunctionCallOutputMap = ({
|
||||
aiRequest,
|
@@ -36,7 +36,7 @@ import { type EditorCallbacks } from '../EditorFunctions';
|
||||
import {
|
||||
getFunctionCallOutputsFromEditorFunctionCallResults,
|
||||
getFunctionCallsToProcess,
|
||||
} from './AiRequestChat/AiRequestUtils';
|
||||
} from './AiRequestUtils';
|
||||
import { useStableUpToDateRef } from '../Utils/UseStableUpToDateCallback';
|
||||
import { useTriggerAtNextRender } from '../Utils/useTriggerAtNextRender';
|
||||
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
@@ -52,66 +52,10 @@ import {
|
||||
import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const useEditorFunctionCallResultsPerRequest = () => {
|
||||
const [
|
||||
editorFunctionCallResultsPerRequest,
|
||||
setEditorFunctionCallResultsPerRequest,
|
||||
] = React.useState<{
|
||||
[aiRequestId: string]: Array<EditorFunctionCallResult>,
|
||||
}>({});
|
||||
|
||||
return {
|
||||
getEditorFunctionCallResults: React.useCallback(
|
||||
(aiRequestId: string): Array<EditorFunctionCallResult> | null =>
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || null,
|
||||
[editorFunctionCallResultsPerRequest]
|
||||
),
|
||||
addEditorFunctionCallResults: React.useCallback(
|
||||
(
|
||||
aiRequestId: string,
|
||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||
) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => {
|
||||
const existingEditorFunctionCallResults = (
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || []
|
||||
).filter(existingEditorFunctionCallResult => {
|
||||
return !editorFunctionCallResults.some(
|
||||
editorFunctionCallResult => {
|
||||
return (
|
||||
editorFunctionCallResult.call_id ===
|
||||
existingEditorFunctionCallResult.call_id
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: [
|
||||
...existingEditorFunctionCallResults,
|
||||
...editorFunctionCallResults,
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
[]
|
||||
),
|
||||
clearEditorFunctionCallResults: React.useCallback((aiRequestId: string) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => ({
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: null,
|
||||
})
|
||||
);
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
|
||||
const useProcessFunctionCalls = ({
|
||||
i18n,
|
||||
project,
|
||||
@@ -129,7 +73,9 @@ const useProcessFunctionCalls = ({
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
selectedAiRequest: ?AiRequest,
|
||||
onSendEditorFunctionCallResults: () => Promise<void>,
|
||||
onSendEditorFunctionCallResults: (
|
||||
options: null | {| createdSceneNames: Array<string> |}
|
||||
) => Promise<void>,
|
||||
getEditorFunctionCallResults: string => Array<EditorFunctionCallResult> | null,
|
||||
addEditorFunctionCallResults: (
|
||||
string,
|
||||
@@ -196,7 +142,7 @@ const useProcessFunctionCalls = ({
|
||||
}))
|
||||
);
|
||||
|
||||
const editorFunctionCallResults = await processEditorFunctionCalls({
|
||||
const { results, createdSceneNames } = await processEditorFunctionCalls({
|
||||
project,
|
||||
editorCallbacks,
|
||||
functionCalls: functionCalls.map(functionCall => ({
|
||||
@@ -216,14 +162,13 @@ const useProcessFunctionCalls = ({
|
||||
searchAndInstallAsset,
|
||||
});
|
||||
|
||||
addEditorFunctionCallResults(
|
||||
selectedAiRequest.id,
|
||||
editorFunctionCallResults
|
||||
);
|
||||
addEditorFunctionCallResults(selectedAiRequest.id, results);
|
||||
|
||||
// We may have processed everything, so try to send the results
|
||||
// to the backend.
|
||||
triggerSendEditorFunctionCallResults();
|
||||
triggerSendEditorFunctionCallResults({
|
||||
createdSceneNames,
|
||||
});
|
||||
},
|
||||
[
|
||||
project,
|
||||
@@ -280,52 +225,20 @@ const useProcessFunctionCalls = ({
|
||||
};
|
||||
};
|
||||
|
||||
type AiRequestSendState = {|
|
||||
isSending: boolean,
|
||||
lastSendError: ?Error,
|
||||
|};
|
||||
|
||||
export const useAiRequests = () => {
|
||||
export const useSelectedAiRequest = ({
|
||||
initialAiRequestId,
|
||||
}: {|
|
||||
initialAiRequestId: string | null,
|
||||
|}) => {
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const { aiRequestStorage } = React.useContext(AiRequestContext);
|
||||
const { aiRequests, updateAiRequest } = aiRequestStorage;
|
||||
|
||||
const [aiRequests, setAiRequests] = React.useState<{ [string]: AiRequest }>(
|
||||
{}
|
||||
);
|
||||
const [selectedAiRequestId, setSelectedAiRequestId] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const updateAiRequest = React.useCallback(
|
||||
(aiRequestId: string, aiRequest: AiRequest) => {
|
||||
setAiRequests(aiRequests => ({
|
||||
...aiRequests,
|
||||
[aiRequestId]: aiRequest,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const refreshAiRequest = React.useCallback(
|
||||
async (aiRequestId: string) => {
|
||||
if (!profile) return;
|
||||
|
||||
try {
|
||||
const updatedAiRequest = await getAiRequest(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
aiRequestId: aiRequestId,
|
||||
});
|
||||
updateAiRequest(updatedAiRequest.id, updatedAiRequest);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error while background refreshing AI request - ignoring:',
|
||||
error
|
||||
);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, profile, updateAiRequest]
|
||||
);
|
||||
>(initialAiRequestId);
|
||||
|
||||
const selectedAiRequest =
|
||||
(selectedAiRequestId && aiRequests[selectedAiRequestId]) || null;
|
||||
@@ -375,59 +288,10 @@ export const useAiRequests = () => {
|
||||
]
|
||||
);
|
||||
|
||||
const [aiRequestSendStates, setAiRequestSendStates] = React.useState<{
|
||||
[string]: AiRequestSendState,
|
||||
}>({});
|
||||
const isSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
!!aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].isSending,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const getLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
(aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].lastSendError) ||
|
||||
null,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const setSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null, isSending: boolean) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending,
|
||||
lastSendError: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
const setLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null, lastSendError: ?Error) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending: false,
|
||||
lastSendError,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
|
||||
return {
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
setSelectedAiRequestId,
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
setSendingAiRequest,
|
||||
setLastSendError,
|
||||
getLastSendError,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -438,6 +302,8 @@ const styles = {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
chatContainer: {
|
||||
flex: 1,
|
||||
@@ -483,7 +349,13 @@ type Props = {|
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
mode?: 'chat' | 'agent' | null,
|
||||
initialMode: 'chat' | 'agent' | null,
|
||||
initialAiRequestId: string | null,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
export type AskAiEditorInterface = {|
|
||||
@@ -499,7 +371,10 @@ export type AskAiEditorInterface = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
startNewChat: (mode: 'chat' | 'agent') => void,
|
||||
startOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
export type NewAiRequestOptions = {|
|
||||
@@ -525,7 +400,9 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onOpenLayout,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
mode,
|
||||
initialMode,
|
||||
initialAiRequestId,
|
||||
onOpenAskAi,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
@@ -540,13 +417,9 @@ export const AskAiEditor = React.memo<Props>(
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
setSelectedAiRequestId,
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
setSendingAiRequest,
|
||||
isSendingAiRequest,
|
||||
getLastSendError,
|
||||
setLastSendError,
|
||||
} = useAiRequests();
|
||||
} = useSelectedAiRequest({
|
||||
initialAiRequestId,
|
||||
});
|
||||
const upToDateSelectedAiRequestId = useStableUpToDateRef(
|
||||
selectedAiRequestId
|
||||
);
|
||||
@@ -558,26 +431,30 @@ export const AskAiEditor = React.memo<Props>(
|
||||
|
||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState<boolean>(false);
|
||||
const [newChatMode, setNewChatMode] = React.useState<'chat' | 'agent'>(
|
||||
mode || 'agent'
|
||||
initialMode || 'agent'
|
||||
);
|
||||
|
||||
// Update newChatMode when mode prop changes
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (mode) {
|
||||
setNewChatMode(mode);
|
||||
if (initialMode) {
|
||||
setNewChatMode(initialMode);
|
||||
}
|
||||
},
|
||||
[mode]
|
||||
[initialMode]
|
||||
);
|
||||
|
||||
const canStartNewChat = !!selectedAiRequestId;
|
||||
const onStartNewChat = React.useCallback(
|
||||
(mode: 'chat' | 'agent') => {
|
||||
setSelectedAiRequestId(null);
|
||||
if (mode) {
|
||||
setNewChatMode(mode);
|
||||
}
|
||||
const onStartOrOpenChat = React.useCallback(
|
||||
({
|
||||
mode,
|
||||
aiRequestId,
|
||||
}: {|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => {
|
||||
setSelectedAiRequestId(aiRequestId);
|
||||
setNewChatMode(mode);
|
||||
},
|
||||
[setSelectedAiRequestId]
|
||||
);
|
||||
@@ -590,11 +467,23 @@ export const AskAiEditor = React.memo<Props>(
|
||||
setIsHistoryOpen(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
aiRequestStorage,
|
||||
editorFunctionCallResultsStorage,
|
||||
} = React.useContext(AiRequestContext);
|
||||
const {
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
clearEditorFunctionCallResults,
|
||||
} = useEditorFunctionCallResultsPerRequest();
|
||||
} = editorFunctionCallResultsStorage;
|
||||
const {
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
getLastSendError,
|
||||
setSendingAiRequest,
|
||||
setLastSendError,
|
||||
} = aiRequestStorage;
|
||||
|
||||
const {
|
||||
createAiProject,
|
||||
@@ -606,14 +495,14 @@ export const AskAiEditor = React.memo<Props>(
|
||||
if (setToolbar) {
|
||||
setToolbar(
|
||||
<Toolbar
|
||||
onStartNewChat={onStartNewChat}
|
||||
onStartOrOpenChat={onStartOrOpenChat}
|
||||
canStartNewChat={canStartNewChat}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[setToolbar, onStartNewChat, canStartNewChat, onOpenHistory]
|
||||
[setToolbar, onStartOrOpenChat, canStartNewChat, onOpenHistory]
|
||||
);
|
||||
|
||||
React.useEffect(updateToolbar, [updateToolbar]);
|
||||
@@ -626,7 +515,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onSceneObjectEdited: noop,
|
||||
onSceneObjectsDeleted: noop,
|
||||
onSceneEventsModifiedOutsideEditor: noop,
|
||||
startNewChat: onStartNewChat,
|
||||
startOrOpenChat: onStartOrOpenChat,
|
||||
}));
|
||||
|
||||
const aiRequestChatRef = React.useRef<AiRequestChatInterface | null>(
|
||||
@@ -820,6 +709,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
updateAiRequest,
|
||||
createAiProject,
|
||||
newAiRequestOptions,
|
||||
onOpenAskAi,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -838,7 +728,13 @@ export const AskAiEditor = React.memo<Props>(
|
||||
|
||||
// Send the results of the function call outputs, if any, and the user message (if any).
|
||||
const onSendMessage = React.useCallback(
|
||||
async ({ userMessage }: {| userMessage: string |}) => {
|
||||
async ({
|
||||
userMessage,
|
||||
createdSceneNames,
|
||||
}: {|
|
||||
userMessage: string,
|
||||
createdSceneNames?: Array<string>,
|
||||
|}) => {
|
||||
if (
|
||||
!profile ||
|
||||
!selectedAiRequestId ||
|
||||
@@ -950,9 +846,25 @@ export const AskAiEditor = React.memo<Props>(
|
||||
// Ignore limits refresh error.
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedAiRequest && createdSceneNames) {
|
||||
onOpenAskAi({
|
||||
mode: selectedAiRequest.mode || 'agent',
|
||||
aiRequestId: selectedAiRequestId,
|
||||
paneIdentifier: 'right',
|
||||
});
|
||||
createdSceneNames.forEach(sceneName => {
|
||||
onOpenLayout(sceneName, {
|
||||
openEventsEditor: true,
|
||||
openSceneEditor: true,
|
||||
focusWhenOpened: 'scene',
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
profile,
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
isSendingAiRequest,
|
||||
getEditorFunctionCallResults,
|
||||
@@ -968,12 +880,15 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onRefreshLimits,
|
||||
project,
|
||||
hasFunctionsCallsToProcess,
|
||||
onOpenAskAi,
|
||||
onOpenLayout,
|
||||
]
|
||||
);
|
||||
const onSendEditorFunctionCallResults = React.useCallback(
|
||||
() =>
|
||||
(options: null | {| createdSceneNames: Array<string> |}) =>
|
||||
onSendMessage({
|
||||
userMessage: '',
|
||||
createdSceneNames: options ? options.createdSceneNames : [],
|
||||
}),
|
||||
[onSendMessage]
|
||||
);
|
||||
@@ -1067,7 +982,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}}
|
||||
i18n={i18n}
|
||||
editorCallbacks={editorCallbacks}
|
||||
onStartNewChat={onStartNewChat}
|
||||
onStartOrOpenChat={onStartOrOpenChat}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
@@ -1116,7 +1031,13 @@ export const renderAskAiEditorContainer = (
|
||||
props.onSceneEventsModifiedOutsideEditor
|
||||
}
|
||||
onExtensionInstalled={props.onExtensionInstalled}
|
||||
mode={props.extraEditorProps && props.extraEditorProps.mode}
|
||||
initialMode={
|
||||
(props.extraEditorProps && props.extraEditorProps.mode) || null
|
||||
}
|
||||
initialAiRequestId={
|
||||
(props.extraEditorProps && props.extraEditorProps.aiRequestId) || null
|
||||
}
|
||||
onOpenAskAi={props.onOpenAskAi}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -247,7 +247,7 @@ export const AskAiHistory = ({
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
anchor="left"
|
||||
anchor="right"
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
style: styles.drawer,
|
||||
@@ -260,6 +260,7 @@ export const AskAiHistory = ({
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<DrawerTopBar
|
||||
title={<Trans>AI Chat History</Trans>}
|
||||
drawerAnchor="right"
|
||||
id="ai-chat-history-drawer-top-bar"
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
@@ -3,18 +3,21 @@ import { Trans, t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { ToolbarGroup } from '../UI/Toolbar';
|
||||
import AddIcon from '../UI/CustomSvgIcons/Add';
|
||||
import HistoryIcon from '../UI/CustomSvgIcons/History';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import ChatBubblesIcon from '../UI/CustomSvgIcons/ChatBubbles';
|
||||
import ResponsiveRaisedButton from '../UI/ResponsiveRaisedButton';
|
||||
import IconButton from '../UI/IconButton';
|
||||
|
||||
type Props = {|
|
||||
onStartNewChat: (mode: 'chat' | 'agent') => void,
|
||||
onStartOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
canStartNewChat: boolean,
|
||||
onOpenHistory: () => void,
|
||||
|};
|
||||
|
||||
export const Toolbar = ({
|
||||
onStartNewChat,
|
||||
onStartOrOpenChat,
|
||||
canStartNewChat,
|
||||
onOpenHistory,
|
||||
}: Props) => {
|
||||
@@ -27,16 +30,24 @@ export const Toolbar = ({
|
||||
tooltip={t`View history`}
|
||||
onClick={onOpenHistory}
|
||||
>
|
||||
<HistoryIcon />
|
||||
<ChatBubblesIcon />
|
||||
</IconButton>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup lastChild>
|
||||
<RaisedButton
|
||||
<ResponsiveRaisedButton
|
||||
primary
|
||||
onClick={() => onStartNewChat('agent')}
|
||||
onClick={() =>
|
||||
onStartOrOpenChat({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
})
|
||||
}
|
||||
icon={<AddIcon />}
|
||||
label={<Trans>Start a new chat</Trans>}
|
||||
disabled={!canStartNewChat}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</ToolbarGroup>
|
||||
</>
|
||||
|
@@ -56,10 +56,12 @@ export const processEditorFunctionCalls = async ({
|
||||
ignore,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
}: ProcessEditorFunctionCallsOptions): Promise<
|
||||
Array<EditorFunctionCallResult>
|
||||
> => {
|
||||
}: ProcessEditorFunctionCallsOptions): Promise<{|
|
||||
results: Array<EditorFunctionCallResult>,
|
||||
createdSceneNames: Array<string>,
|
||||
|}> => {
|
||||
const results: Array<EditorFunctionCallResult> = [];
|
||||
const createdSceneNames: Array<string> = [];
|
||||
|
||||
for (const functionCall of functionCalls) {
|
||||
const call_id = functionCall.call_id;
|
||||
@@ -147,12 +149,8 @@ export const processEditorFunctionCalls = async ({
|
||||
});
|
||||
|
||||
if (success && args) {
|
||||
if (typeof args.scene_name === 'string') {
|
||||
editorCallbacks.onOpenLayout(args.scene_name, {
|
||||
openEventsEditor: true,
|
||||
openSceneEditor: true,
|
||||
focusWhenOpened: 'none',
|
||||
});
|
||||
if (name === 'create_scene' && typeof args.scene_name === 'string') {
|
||||
createdSceneNames.push(args.scene_name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -165,5 +163,5 @@ export const processEditorFunctionCalls = async ({
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
return { results, createdSceneNames };
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -29,6 +29,7 @@ export type EditorContainerExtraProps = {|
|
||||
|
||||
// Ask AI
|
||||
mode?: 'chat' | 'agent',
|
||||
aiRequestId?: string | null,
|
||||
|};
|
||||
|
||||
export type SceneEventsOutsideEditorChanges = {|
|
||||
@@ -43,7 +44,7 @@ export type RenderEditorContainerProps = {|
|
||||
fileMetadata: ?FileMetadata,
|
||||
storageProvider: StorageProvider,
|
||||
setToolbar: (?React.Node) => void,
|
||||
hideTabsTitleBarAndEditorToolbar: (hidden: boolean) => void,
|
||||
setGamesPlatformFrameShown: ({| shown: boolean, isMobile: boolean |}) => void,
|
||||
|
||||
// Some optional extra props to pass to the rendered editor
|
||||
extraEditorProps: ?EditorContainerExtraProps,
|
||||
@@ -86,7 +87,11 @@ export type RenderEditorContainerProps = {|
|
||||
variantName: string
|
||||
) => void,
|
||||
openObjectEvents: (extensionName: string, objectName: string) => void,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
|
||||
// Events function management:
|
||||
onLoadEventsFunctionsExtensions: () => Promise<void>,
|
||||
|
@@ -115,7 +115,11 @@ type Props = {|
|
||||
purchasingCourseListingData: ?CourseListingData,
|
||||
setPurchasingCourseListingData: (CourseListingData | null) => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
const CourseSection = ({
|
||||
@@ -432,7 +436,13 @@ const CourseSection = ({
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Ask the AI</Trans>}
|
||||
onClick={() => onOpenAskAi('chat')}
|
||||
onClick={() =>
|
||||
onOpenAskAi({
|
||||
mode: 'chat',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'right',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Paper>
|
||||
|
@@ -71,7 +71,11 @@ type Props = {|
|
||||
courseId: string,
|
||||
chapterId: string
|
||||
) => CourseChapterCompletion | null,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
onOpenNewProjectSetupDialog: () => void,
|
||||
onSelectPrivateGameTemplateListingData: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
@@ -391,7 +395,13 @@ const MainPage = ({
|
||||
color="success"
|
||||
label={<Trans>Ask the AI</Trans>}
|
||||
rightIcon={<ArrowRight />}
|
||||
onClick={() => onOpenAskAi('chat')}
|
||||
onClick={() =>
|
||||
onOpenAskAi({
|
||||
mode: 'chat',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
|
@@ -62,7 +62,11 @@ type Props = {|
|
||||
) => Promise<void>,
|
||||
purchasingCourseListingData: ?CourseListingData,
|
||||
setPurchasingCourseListingData: (CourseListingData | null) => void,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
onOpenNewProjectSetupDialog: () => void,
|
||||
onSelectPrivateGameTemplateListingData: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
|
@@ -1,13 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
|
||||
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import {
|
||||
homepageDesktopMenuBarWidth,
|
||||
homepageMediumMenuBarWidth,
|
||||
homepageMobileMenuHeight,
|
||||
} from '../HomePageMenuBar';
|
||||
import { homepageMobileMenuHeight } from '../HomePageMenuBar';
|
||||
import Paper from '../../../../UI/Paper';
|
||||
import { type IframePosition } from './UseGamesPlatformFrame';
|
||||
|
||||
export const GAMES_PLATFORM_IFRAME_ID = 'games-platform-frame';
|
||||
|
||||
@@ -33,12 +29,17 @@ type Props = {|
|
||||
loaded: boolean,
|
||||
visible: boolean,
|
||||
initialGameId: ?string,
|
||||
iframePosition: ?IframePosition,
|
||||
|};
|
||||
|
||||
const GamesPlatformFrame = ({ initialGameId, loaded, visible }: Props) => {
|
||||
const GamesPlatformFrame = ({
|
||||
initialGameId,
|
||||
loaded,
|
||||
visible,
|
||||
iframePosition,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const paletteType = gdevelopTheme.palette.type;
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
|
||||
// Use a ref to store the initial game id, as we don't want to trigger a re-render
|
||||
// when the game id changes.
|
||||
@@ -64,19 +65,19 @@ const GamesPlatformFrame = ({ initialGameId, loaded, visible }: Props) => {
|
||||
[loaded, initialGameId]
|
||||
);
|
||||
|
||||
const titleBarAndToolbarHeight = isMobile ? 0 : 37 + 40;
|
||||
const containerTop = isMobile
|
||||
? 0 // Always top of the screen on small screens.
|
||||
: `calc(${titleBarAndToolbarHeight}px + var(--safe-area-inset-top))`;
|
||||
const containerBottom = isMobile ? homepageMobileMenuHeight : 0;
|
||||
const containerLeft = isMobile
|
||||
? 0
|
||||
: isMediumScreen
|
||||
? homepageMediumMenuBarWidth
|
||||
: homepageDesktopMenuBarWidth;
|
||||
const containerWidth = `calc(100% - ${containerLeft}px`;
|
||||
const containerHeight = `calc(100% - ${titleBarAndToolbarHeight +
|
||||
containerBottom}px - ${isMobile ? '0px' : 'var(--safe-area-inset-top)'})`;
|
||||
// In this component, do not use useResponsiveWindowSize. The position
|
||||
// of the iframe is read from iframePosition - which is set by the component
|
||||
// responsible for positioning the iframe (typically, the "Play section" component).
|
||||
// The iframe position can change according to the size of the pane containing it -
|
||||
// which is not always the same as the size of the top level window.
|
||||
const containerTop =
|
||||
iframePosition && !iframePosition.isMobile ? iframePosition.top : 0;
|
||||
const containerLeft = iframePosition ? iframePosition.left : 0;
|
||||
const containerWidth = iframePosition ? iframePosition.width : '100%';
|
||||
const containerHeight =
|
||||
iframePosition && !iframePosition.isMobile
|
||||
? iframePosition.height
|
||||
: `calc(100% - ${homepageMobileMenuHeight}px)`;
|
||||
|
||||
// We wrap the iframe in a paper, as its content has a transparent background,
|
||||
// and we don't want what's behind the iframe to be visible.
|
||||
|
@@ -163,12 +163,21 @@ const sendSoftKeyboardOffsetToFrame = async (offset: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
export type IframePosition = {|
|
||||
isMobile: boolean,
|
||||
top: number,
|
||||
left: number,
|
||||
width: number,
|
||||
height: number,
|
||||
|};
|
||||
|
||||
export type GamesPlatformFrameTools = {|
|
||||
startTimeoutToUnloadIframe: () => void,
|
||||
loadIframeOrRemoveTimeout: () => void,
|
||||
iframeLoaded: boolean,
|
||||
iframeVisible: boolean,
|
||||
iframeErrored: boolean,
|
||||
updateIframePosition: (position: IframePosition) => void,
|
||||
renderGamesPlatformFrame: () => React.Node,
|
||||
|};
|
||||
|
||||
@@ -185,6 +194,9 @@ const useGamesPlatformFrame = ({
|
||||
const [iframeVisible, setIframeVisible] = React.useState(false);
|
||||
const [iframeLoaded, setIframeLoaded] = React.useState(false);
|
||||
const [iframeErrored, setIframeErrored] = React.useState(false);
|
||||
const [iframePosition, setIframePosition] = React.useState<?IframePosition>(
|
||||
null
|
||||
);
|
||||
const [lastGameId, setLastGameId] = React.useState<?string>(null);
|
||||
const timeoutToUnloadIframe = React.useRef<?TimeoutID>(null);
|
||||
const { openUserPublicProfile } = React.useContext(PublicProfileContext);
|
||||
@@ -504,9 +516,26 @@ const useGamesPlatformFrame = ({
|
||||
initialGameId={lastGameId}
|
||||
loaded={loadIframeInDOM}
|
||||
visible={iframeVisible}
|
||||
iframePosition={iframePosition}
|
||||
/>
|
||||
),
|
||||
[lastGameId, loadIframeInDOM, iframeVisible]
|
||||
[lastGameId, loadIframeInDOM, iframeVisible, iframePosition]
|
||||
);
|
||||
|
||||
const updateIframePosition = React.useCallback(
|
||||
(position: IframePosition) => {
|
||||
if (
|
||||
!iframePosition ||
|
||||
iframePosition.isMobile !== position.isMobile ||
|
||||
iframePosition.top !== position.top ||
|
||||
iframePosition.left !== position.left ||
|
||||
iframePosition.width !== position.width ||
|
||||
iframePosition.height !== position.height
|
||||
) {
|
||||
setIframePosition(position);
|
||||
}
|
||||
},
|
||||
[setIframePosition, iframePosition]
|
||||
);
|
||||
|
||||
const gamesPlatformFrameTools = React.useMemo(
|
||||
@@ -516,6 +545,7 @@ const useGamesPlatformFrame = ({
|
||||
iframeLoaded,
|
||||
iframeVisible,
|
||||
iframeErrored,
|
||||
updateIframePosition,
|
||||
renderGamesPlatformFrame,
|
||||
}),
|
||||
[
|
||||
@@ -524,6 +554,7 @@ const useGamesPlatformFrame = ({
|
||||
iframeLoaded,
|
||||
iframeVisible,
|
||||
iframeErrored,
|
||||
updateIframePosition,
|
||||
renderGamesPlatformFrame,
|
||||
]
|
||||
);
|
||||
|
@@ -7,24 +7,51 @@ import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../../../../UI/PlaceholderError';
|
||||
import { sendPlaySectionOpened } from '../../../../Utils/Analytics/EventSender';
|
||||
import { type GamesPlatformFrameTools } from './UseGamesPlatformFrame';
|
||||
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
|
||||
type Props = {|
|
||||
gamesPlatformFrameTools: GamesPlatformFrameTools,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
iframeTarget: { position: 'absolute', inset: 0, pointerEvents: 'none' },
|
||||
};
|
||||
|
||||
const PlaySection = ({ gamesPlatformFrameTools }: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const {
|
||||
iframeLoaded,
|
||||
iframeErrored,
|
||||
loadIframeOrRemoveTimeout,
|
||||
updateIframePosition,
|
||||
} = gamesPlatformFrameTools;
|
||||
|
||||
// At each render, communicate the iframe position to the games frame.
|
||||
// If on a "mobile" screen (either a real mobile, or a desktop that has a small
|
||||
// window, or an editor tab that is too narrow), communicate this so that the
|
||||
// iframe can be shown from the top of the screen.
|
||||
const iframePositionRef = React.useRef<?HTMLDivElement>(null);
|
||||
React.useLayoutEffect(() => {
|
||||
if (iframePositionRef.current) {
|
||||
const rect = iframePositionRef.current.getBoundingClientRect();
|
||||
updateIframePosition({
|
||||
isMobile,
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
sendPlaySectionOpened();
|
||||
}, []);
|
||||
|
||||
// Iframe will be displayed here if loaded.
|
||||
return iframeLoaded ? null : (
|
||||
return iframeLoaded ? (
|
||||
<div style={styles.iframeTarget} ref={iframePositionRef} />
|
||||
) : (
|
||||
<SectionContainer flexBody>
|
||||
<SectionRow expand>
|
||||
{iframeErrored ? (
|
||||
|
@@ -85,6 +85,7 @@ const styles = {
|
||||
},
|
||||
scrollableContainer: {
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
flexDirection: 'column',
|
||||
@@ -104,7 +105,7 @@ type Props = {|
|
||||
projectItemName: ?string,
|
||||
project: ?gdProject,
|
||||
setToolbar: (?React.Node) => void,
|
||||
hideTabsTitleBarAndEditorToolbar: (hidden: boolean) => void,
|
||||
setGamesPlatformFrameShown: ({| shown: boolean, isMobile: boolean |}) => void,
|
||||
storageProviders: Array<StorageProvider>,
|
||||
|
||||
// Games
|
||||
@@ -132,7 +133,11 @@ type Props = {|
|
||||
selectInAppTutorial: (tutorialId: string) => void,
|
||||
onOpenPreferences: () => void,
|
||||
onOpenAbout: () => void,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
|
||||
// Project creation
|
||||
onOpenNewProjectSetupDialog: () => void,
|
||||
@@ -191,7 +196,7 @@ export const HomePage = React.memo<Props>(
|
||||
onOpenProfile,
|
||||
onCreateProjectFromExample,
|
||||
setToolbar,
|
||||
hideTabsTitleBarAndEditorToolbar,
|
||||
setGamesPlatformFrameShown,
|
||||
selectInAppTutorial,
|
||||
onOpenPreferences,
|
||||
onOpenAbout,
|
||||
@@ -420,19 +425,19 @@ export const HomePage = React.memo<Props>(
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
// Hide the toolbars when on mobile in the "play" tab.
|
||||
if (activeTab === 'play' && isMobile) {
|
||||
hideTabsTitleBarAndEditorToolbar(true);
|
||||
if (activeTab === 'play') {
|
||||
setGamesPlatformFrameShown({ shown: true, isMobile });
|
||||
} else {
|
||||
hideTabsTitleBarAndEditorToolbar(false);
|
||||
setGamesPlatformFrameShown({ shown: false, isMobile });
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
// Ensure we show it again when the tab changes.
|
||||
return () => {
|
||||
hideTabsTitleBarAndEditorToolbar(false);
|
||||
setGamesPlatformFrameShown({ shown: false, isMobile });
|
||||
};
|
||||
},
|
||||
[updateToolbar, activeTab, hideTabsTitleBarAndEditorToolbar, isMobile]
|
||||
[updateToolbar, activeTab, setGamesPlatformFrameShown, isMobile]
|
||||
);
|
||||
|
||||
const forceUpdateEditor = React.useCallback(() => {
|
||||
@@ -650,7 +655,7 @@ export const renderHomePageContainer = (
|
||||
isActive={props.isActive}
|
||||
projectItemName={props.projectItemName}
|
||||
setToolbar={props.setToolbar}
|
||||
hideTabsTitleBarAndEditorToolbar={props.hideTabsTitleBarAndEditorToolbar}
|
||||
setGamesPlatformFrameShown={props.setGamesPlatformFrameShown}
|
||||
canOpen={props.canOpen}
|
||||
onChooseProject={props.onChooseProject}
|
||||
onOpenRecentFile={props.onOpenRecentFile}
|
||||
|
@@ -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,11 @@ export function DraggableEditorTabs({
|
||||
<ClosableTabs
|
||||
hideLabels={hideLabels}
|
||||
renderTabs={({ containerWidth }) => {
|
||||
const editors = getEditors(editorTabs);
|
||||
const hasHomeTab =
|
||||
editors.length > 0 && editors[0].key === 'start page';
|
||||
|
||||
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
|
||||
@@ -94,7 +90,10 @@ export function DraggableEditorTabs({
|
||||
const maxWidth = Math.max(
|
||||
minimumMaxWidth,
|
||||
// The home tab is special because it's just an icon.
|
||||
(containerWidth - homeTabApproximateWidth) / (editors.length - 1)
|
||||
hasHomeTab
|
||||
? (containerWidth - homeTabApproximateWidth) /
|
||||
(editors.length - 1)
|
||||
: containerWidth / editors.length
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -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,20 @@ export const getEditorTabMetadata = (
|
||||
|
||||
export const getEditorTabsInitialState = (): EditorTabsState => {
|
||||
return {
|
||||
editors: [],
|
||||
currentTab: 0,
|
||||
panes: {
|
||||
left: {
|
||||
editors: [],
|
||||
currentTab: 0,
|
||||
},
|
||||
center: {
|
||||
editors: [],
|
||||
currentTab: 0,
|
||||
},
|
||||
right: {
|
||||
editors: [],
|
||||
currentTab: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -144,17 +161,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 +198,82 @@ 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);
|
||||
const currentEditorTabNewIndex = paneRemainingEditors.indexOf(
|
||||
currentEditorTab
|
||||
);
|
||||
newState.panes[paneIdentifier] = {
|
||||
...pane,
|
||||
editors: paneRemainingEditors,
|
||||
|
||||
// Keep the focus on the current editor tab, or if it was closed
|
||||
// go back to the first tab.
|
||||
currentTab:
|
||||
currentEditorTabNewIndex === -1 ? 0 : currentEditorTabNewIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const closeAllEditorTabs = (state: EditorTabsState): EditorTabsState => {
|
||||
@@ -233,16 +297,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 +336,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 +359,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 +494,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 +520,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 +545,7 @@ export const getCustomObjectEditor = (
|
||||
|
||||
export const moveTabToTheRightOfHoveredTab = (
|
||||
editorTabsState: EditorTabsState,
|
||||
paneIdentifier: string,
|
||||
movingTabIndex: number,
|
||||
hoveredTabIndex: number
|
||||
): EditorTabsState => {
|
||||
@@ -450,21 +554,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 +587,86 @@ 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 = (
|
||||
export const getEditorTabOpenedWithKey = (
|
||||
editorTabsState: EditorTabsState,
|
||||
key: string
|
||||
) => {
|
||||
return !!editorTabsState.editors.find(editor => editor.key === key);
|
||||
): {|
|
||||
paneIdentifier: string,
|
||||
editorTab: EditorTab,
|
||||
|} | null => {
|
||||
for (const paneIdentifier in editorTabsState.panes) {
|
||||
const pane = editorTabsState.panes[paneIdentifier];
|
||||
const editorTab = pane && pane.editors.find(editor => editor.key === key);
|
||||
if (editorTab) {
|
||||
return { editorTab, paneIdentifier };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const hasEditorTabOpenedWithKey = (
|
||||
editorTabsState: EditorTabsState,
|
||||
key: string
|
||||
): boolean => {
|
||||
return getEditorTabOpenedWithKey(editorTabsState, key) !== null;
|
||||
};
|
||||
|
||||
export const getOpenedAskAiEditor = (
|
||||
state: EditorTabsState
|
||||
): AskAiEditorInterface | null => {
|
||||
const editor = state.editors.find(editor => editor.key === 'ask-ai');
|
||||
): null | {|
|
||||
askAiEditor: AskAiEditorInterface,
|
||||
editorTab: EditorTab,
|
||||
paneIdentifier: string,
|
||||
|} => {
|
||||
const currentEditorTabAndPaneIdentifier = getEditorTabOpenedWithKey(
|
||||
state,
|
||||
'ask-ai'
|
||||
);
|
||||
if (!currentEditorTabAndPaneIdentifier) return null;
|
||||
|
||||
// $FlowFixMe - the key ensures that the editor is an AskAiEditorInterface.
|
||||
return (editor && editor.editorRef) || null;
|
||||
return {
|
||||
// $FlowFixMe - the key ensures that the editor is an AskAiEditorInterface.
|
||||
askAiEditor: currentEditorTabAndPaneIdentifier.editorTab.editorRef,
|
||||
editorTab: currentEditorTabAndPaneIdentifier.editorTab,
|
||||
paneIdentifier: currentEditorTabAndPaneIdentifier.paneIdentifier,
|
||||
};
|
||||
};
|
||||
|
||||
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 hasEditorsInPane = (
|
||||
state: EditorTabsState,
|
||||
paneIdentifier: string
|
||||
): boolean => {
|
||||
const pane = state.panes[paneIdentifier];
|
||||
if (!pane) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pane.editors.length > 0;
|
||||
};
|
||||
|
@@ -23,7 +23,9 @@ type Props = {|
|
||||
name: string,
|
||||
dontFocusTab?: boolean,
|
||||
project?: ?gdProject,
|
||||
paneIdentifier?: 'left' | 'center' | 'right' | null,
|
||||
mode?: 'chat' | 'agent',
|
||||
aiRequestId?: string | null,
|
||||
|}) => EditorOpeningOptions,
|
||||
|};
|
||||
|
||||
@@ -77,11 +79,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),
|
||||
};
|
||||
@@ -175,9 +178,10 @@ const useEditorTabsStateSaving = ({
|
||||
}
|
||||
newEditorTabs = changeCurrentTab(
|
||||
newEditorTabs,
|
||||
'center',
|
||||
shouldOpenSavedCurrentTab
|
||||
? editorState.editorTabs.currentTab
|
||||
: newEditorTabs.editors.length >= 1
|
||||
: newEditorTabs.panes.center.editors.length >= 1
|
||||
? 1
|
||||
: 0
|
||||
);
|
||||
|
699
newIDE/app/src/MainFrame/EditorTabsPane.js
Normal file
699
newIDE/app/src/MainFrame/EditorTabsPane.js
Normal file
@@ -0,0 +1,699 @@
|
||||
// @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,
|
||||
getEditorTabOpenedWithKey,
|
||||
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';
|
||||
import { SpecificDimensionsWindowSizeProvider } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { MuiThemeOnlyProvider } from '../UI/Theme/FullThemeProvider';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import useOnResize from '../Utils/UseOnResize';
|
||||
import DrawerTopBar from '../UI/DrawerTopBar';
|
||||
import { type FloatingPaneState } from './PanesContainer';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export type EditorTabsPaneCommonProps = {|
|
||||
editorTabs: EditorTabsState,
|
||||
currentProject: ?gdProject,
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
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>,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => 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,
|
||||
onSetPointerEventsNone: (enablePointerEventsNone: boolean) => void,
|
||||
paneIdentifier: string,
|
||||
isLeftMostPane: boolean,
|
||||
isRightMostPane: boolean,
|
||||
isDrawer: boolean,
|
||||
areSidePanesDrawers: boolean,
|
||||
onSetPaneDrawerState: (
|
||||
paneIdentifier: string,
|
||||
newState: FloatingPaneState
|
||||
) => void,
|
||||
|};
|
||||
|
||||
const EditorTabsPane = React.forwardRef<Props, {||}>((props, ref) => {
|
||||
const {
|
||||
editorTabs,
|
||||
currentProject,
|
||||
currentFileMetadata,
|
||||
canSave,
|
||||
isSavingProject,
|
||||
isSharingEnabled,
|
||||
hasPreviewsRunning,
|
||||
previewState,
|
||||
checkedOutVersionStatus,
|
||||
canDoNetworkPreview,
|
||||
gamesPlatformFrameTools,
|
||||
toggleProjectManager,
|
||||
saveProject,
|
||||
openShareDialog,
|
||||
launchDebuggerAndPreview,
|
||||
launchNewPreview,
|
||||
launchNetworkPreview,
|
||||
launchHotReloadPreview,
|
||||
launchPreviewWithDiagnosticReport,
|
||||
setPreviewOverride,
|
||||
openVersionHistoryPanel,
|
||||
onQuitVersionHistory,
|
||||
onOpenAskAi,
|
||||
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,
|
||||
onSetPointerEventsNone,
|
||||
paneIdentifier,
|
||||
isLeftMostPane,
|
||||
isRightMostPane,
|
||||
isDrawer,
|
||||
onSetPaneDrawerState,
|
||||
areSidePanesDrawers,
|
||||
} = props;
|
||||
|
||||
const toolbarRef = React.useRef<?ToolbarInterface>(null);
|
||||
const unsavedChanges = React.useContext(UnsavedChangesContext);
|
||||
const askAiPaneIdentifier = getEditorTabOpenedWithKey(editorTabs, 'ask-ai');
|
||||
const containerRef = React.useRef<?HTMLDivElement>(null);
|
||||
|
||||
const [
|
||||
tabsTitleBarAndEditorToolbarHidden,
|
||||
setTabsTitleBarAndEditorToolbarHidden,
|
||||
] = React.useState(false);
|
||||
|
||||
const onSetGamesPlatformFrameShown = React.useCallback(
|
||||
({ shown, isMobile }: {| shown: boolean, isMobile: boolean |}) => {
|
||||
onSetPointerEventsNone(shown);
|
||||
setTabsTitleBarAndEditorToolbarHidden(shown && isMobile);
|
||||
},
|
||||
[onSetPointerEventsNone]
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// Use a layout effect to read the pane width and height, which is then used
|
||||
// to communicate to children editors the dimensions of their "window" (the pane).
|
||||
// The layout effect ensures that we get the pane width and height after the pane has been rendered
|
||||
// but before the browser repaints the component.
|
||||
const [paneWidth, setPaneWidth] = React.useState<number | null>(null);
|
||||
const [paneHeight, setPaneHeight] = React.useState<number | null>(null);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
React.useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
if (
|
||||
container.clientWidth !== paneWidth ||
|
||||
container.clientHeight !== paneHeight
|
||||
) {
|
||||
setPaneWidth(container.clientWidth);
|
||||
setPaneHeight(container.clientHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
useOnResize(useForceUpdate()); // Ensure the pane is re-rendered when the window is resized.
|
||||
|
||||
const onOpenAskAiFromTitlebar = React.useCallback(
|
||||
() => {
|
||||
if (
|
||||
askAiPaneIdentifier &&
|
||||
askAiPaneIdentifier.paneIdentifier === 'right'
|
||||
) {
|
||||
onSetPaneDrawerState('right', 'open');
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: currentProject ? 'right' : 'center',
|
||||
});
|
||||
},
|
||||
[askAiPaneIdentifier, onOpenAskAi, currentProject, onSetPaneDrawerState]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={styles.container} ref={containerRef}>
|
||||
{isDrawer ? (
|
||||
<DrawerTopBar
|
||||
drawerAnchor={isRightMostPane ? 'right' : 'left'}
|
||||
title={'Ask AI'}
|
||||
id={paneIdentifier + '-top-bar'}
|
||||
onClose={() => onSetPaneDrawerState(paneIdentifier, 'closed')}
|
||||
/>
|
||||
) : (
|
||||
<TabsTitlebar
|
||||
isLeftMostPane={isLeftMostPane}
|
||||
isRightMostPane={isRightMostPane}
|
||||
displayMenuIcon={paneIdentifier === 'center'}
|
||||
hidden={tabsTitleBarAndEditorToolbarHidden}
|
||||
toggleProjectManager={toggleProjectManager}
|
||||
renderTabs={(onEditorTabHovered, onEditorTabClosing) => (
|
||||
<DraggableEditorTabs
|
||||
hideLabels={false}
|
||||
editors={paneEditorTabs}
|
||||
currentTab={currentTab}
|
||||
onClickTab={onChangeEditorTab}
|
||||
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={onEditorTabActivated}
|
||||
onDropTab={onDropEditorTab}
|
||||
onHoverTab={onEditorTabHovered}
|
||||
/>
|
||||
)}
|
||||
displayAskAi={
|
||||
!askAiPaneIdentifier
|
||||
? // If Ask AI is closed, display the button on the right most part of the window.
|
||||
isRightMostPane
|
||||
: // If it's open, only show it if it's in a drawer pane.
|
||||
areSidePanesDrawers &&
|
||||
askAiPaneIdentifier.paneIdentifier !== 'center'
|
||||
}
|
||||
onAskAiClicked={onOpenAskAiFromTitlebar}
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
<SpecificDimensionsWindowSizeProvider
|
||||
innerWidth={paneWidth}
|
||||
innerHeight={paneHeight}
|
||||
>
|
||||
<MuiThemeOnlyProvider>
|
||||
{paneEditorTabs.map((editorTab, id) => {
|
||||
const isCurrentTab =
|
||||
getCurrentTabIndexForPane(editorTabs, paneIdentifier) === id;
|
||||
const errorBoundaryProps = getEditorErrorBoundaryProps(
|
||||
editorTab.key
|
||||
);
|
||||
|
||||
return (
|
||||
<TabContentContainer key={editorTab.key} active={isCurrentTab}>
|
||||
<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),
|
||||
setGamesPlatformFrameShown: onSetGamesPlatformFrameShown,
|
||||
projectItemName: editorTab.projectItemName,
|
||||
setPreviewedLayout,
|
||||
onOpenAskAi,
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</MuiThemeOnlyProvider>
|
||||
</SpecificDimensionsWindowSizeProvider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default EditorTabsPane;
|
@@ -28,6 +28,10 @@ html {
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@@ -46,7 +46,7 @@ export type MainMenuCallbacks = {|
|
||||
onOpenPreferences: (open?: boolean) => void,
|
||||
onOpenLanguage: (open?: boolean) => void,
|
||||
onOpenProfile: (open?: boolean) => void,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: () => void,
|
||||
setElectronUpdateStatus: ElectronUpdateStatus => void,
|
||||
|};
|
||||
|
||||
|
@@ -0,0 +1,105 @@
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.leftPane {
|
||||
flex-basis: 300px;
|
||||
flex-shrink: 0;
|
||||
z-index: 1; /* Create a new stacking context. */
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
flex-basis: 300px;
|
||||
flex-shrink: 0;
|
||||
z-index: 1; /* Create a new stacking context. */
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
transform: translateX(40px);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-left {
|
||||
from {
|
||||
transform: translateX(-40px);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pane opening (drawer or not) */
|
||||
.rightPane:not(.hidden) {
|
||||
animation: slide-in-right 0.15s ease-out;
|
||||
}
|
||||
|
||||
.leftPane:not(.hidden) {
|
||||
animation: slide-in-left 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* Pane as drawer */
|
||||
.pane.drawer {
|
||||
width: min(calc(100vw - 82px), 320px);
|
||||
will-change: transform;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2),
|
||||
0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.rightPane.drawer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.leftPane.drawer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Pane, as drawer, closed. */
|
||||
.rightPane:not(.hidden).closedDrawer {
|
||||
transform: translateX(100vw);
|
||||
}
|
||||
|
||||
.leftPane:not(.hidden).closedDrawer {
|
||||
transform: translateX(-100vw);
|
||||
}
|
||||
|
||||
.centerPane {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
z-index: 1; /* To allow the shadow to be visible. */
|
||||
width: 2px;
|
||||
cursor: ew-resize;
|
||||
background-color: transparent;
|
||||
flex-shrink: 0;
|
||||
pointer-events: all;
|
||||
}
|
||||
.resizer:hover,
|
||||
.resizer:focus {
|
||||
outline: none;
|
||||
}
|
474
newIDE/app/src/MainFrame/PanesContainer/index.js
Normal file
474
newIDE/app/src/MainFrame/PanesContainer/index.js
Normal file
@@ -0,0 +1,474 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import classes from './PanesContainer.module.css';
|
||||
import classNames from 'classnames';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import { useDebounce } from '../../Utils/UseDebounce';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
|
||||
const styles = {
|
||||
pointerEventsNone: {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
pointerEventsAll: {
|
||||
pointerEvents: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
export type FloatingPaneState = 'open' | 'closed';
|
||||
|
||||
type Props = {|
|
||||
renderPane: ({
|
||||
paneIdentifier: string,
|
||||
isLeftMostPane: boolean,
|
||||
isRightMostPane: boolean,
|
||||
isDrawer: boolean,
|
||||
areSidePanesDrawers: boolean,
|
||||
onSetPointerEventsNone: (enablePointerEventsNone: boolean) => void,
|
||||
onSetPaneDrawerState: (
|
||||
paneIdentifier: string,
|
||||
newState: FloatingPaneState
|
||||
) => void,
|
||||
}) => React.Node,
|
||||
hasEditorsInLeftPane: boolean,
|
||||
hasEditorsInRightPane: boolean,
|
||||
|};
|
||||
|
||||
type DraggingState = {|
|
||||
paneIdentifier: 'left' | 'right',
|
||||
startClientX: number,
|
||||
startWidth: number,
|
||||
|};
|
||||
|
||||
const paneWidthMin = 300;
|
||||
|
||||
/** Allow a pane to be moved and closed with a swipe on touchscreens. */
|
||||
const useSwipeableDrawer = ({
|
||||
enabled,
|
||||
paneRef,
|
||||
direction,
|
||||
onClose,
|
||||
}: {|
|
||||
enabled: boolean,
|
||||
paneRef: {| current: HTMLDivElement | null |},
|
||||
direction: 'left' | 'right',
|
||||
onClose: () => void,
|
||||
|}) => {
|
||||
React.useEffect(
|
||||
() => {
|
||||
const drawer = paneRef.current;
|
||||
if (!drawer) return;
|
||||
if (!enabled) return;
|
||||
|
||||
let startX = 0;
|
||||
let currentX = 0;
|
||||
let isDragging = false;
|
||||
|
||||
const minDistanceForAction = 80;
|
||||
const minDistanceForMovement = 30;
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
startX = e.touches[0].clientX;
|
||||
currentX = e.touches[0].clientX;
|
||||
isDragging = true;
|
||||
drawer.style.transition = 'none';
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
currentX = e.touches[0].clientX;
|
||||
const deltaX = currentX - startX;
|
||||
|
||||
const hasMovedEnough = Math.abs(deltaX) > minDistanceForMovement;
|
||||
|
||||
const translate = !hasMovedEnough
|
||||
? 0
|
||||
: direction === 'left'
|
||||
? Math.max(0, deltaX)
|
||||
: Math.min(0, deltaX); // Prevent dragging in wrong direction
|
||||
|
||||
drawer.style.transform = `translateX(${translate}px)`;
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
|
||||
const deltaX = currentX - startX;
|
||||
const shouldClose =
|
||||
direction === 'left'
|
||||
? deltaX > minDistanceForAction
|
||||
: deltaX < -minDistanceForAction;
|
||||
|
||||
// Animate the drawer either to close or fully open position,
|
||||
const animationTimeInMs = 200;
|
||||
drawer.style.transition = `transform 0.${animationTimeInMs}s ease-out`;
|
||||
|
||||
if (shouldClose) {
|
||||
// Animate to close position.
|
||||
const closeTransform = direction === 'left' ? '100vw' : '-100vw';
|
||||
drawer.style.transform = `translateX(${closeTransform})`;
|
||||
|
||||
// Delay to match animation time.
|
||||
setTimeout(() => {
|
||||
onClose(); // This is responsible for ensuring the drawer will stay closed.
|
||||
drawer.style.transform = ''; // Reset for future swipes
|
||||
}, 250);
|
||||
} else {
|
||||
// Snap back to open.
|
||||
drawer.style.transform = 'translateX(0)';
|
||||
|
||||
// Delay to match animation time.
|
||||
setTimeout(() => {
|
||||
drawer.style.transform = ''; // Reset for future swipes
|
||||
}, 250);
|
||||
}
|
||||
};
|
||||
|
||||
drawer.addEventListener('touchstart', onTouchStart);
|
||||
drawer.addEventListener('touchmove', onTouchMove);
|
||||
drawer.addEventListener('touchend', onTouchEnd);
|
||||
|
||||
return () => {
|
||||
drawer.removeEventListener('touchstart', onTouchStart);
|
||||
drawer.removeEventListener('touchmove', onTouchMove);
|
||||
drawer.removeEventListener('touchend', onTouchEnd);
|
||||
drawer.style.transform = '';
|
||||
};
|
||||
},
|
||||
[enabled, paneRef, direction, onClose]
|
||||
);
|
||||
};
|
||||
|
||||
export const PanesContainer = ({
|
||||
renderPane,
|
||||
hasEditorsInLeftPane,
|
||||
hasEditorsInRightPane,
|
||||
}: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const forceUpdate = useForceUpdate();
|
||||
const debouncedForceUpdate = useDebounce(forceUpdate, 200);
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const leftResizerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const leftPaneRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const centerPaneRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const rightPaneRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const rightResizerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const areSidePanesDrawers = isMobile;
|
||||
|
||||
const [panesDrawerState, setPanesDrawerState] = React.useState<{
|
||||
[string]: FloatingPaneState,
|
||||
}>({
|
||||
left: 'open',
|
||||
right: 'open',
|
||||
});
|
||||
|
||||
const setPaneDrawerState = React.useCallback(
|
||||
(paneIdentifier: string, newState: FloatingPaneState) => {
|
||||
setPanesDrawerState(panesDrawerState => ({
|
||||
...panesDrawerState,
|
||||
[paneIdentifier]: newState,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (areSidePanesDrawers) {
|
||||
// Just switched to mobile view: any drawer is closed.
|
||||
setPanesDrawerState({
|
||||
left: 'closed',
|
||||
right: 'closed',
|
||||
});
|
||||
} else {
|
||||
// Just switched to non-mobile view: always consider the pane drawers as open.
|
||||
setPanesDrawerState({
|
||||
left: 'open',
|
||||
right: 'open',
|
||||
});
|
||||
}
|
||||
},
|
||||
[areSidePanesDrawers]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (hasEditorsInRightPane) {
|
||||
// Just opened the first editor in the right pane: ensure the drawer
|
||||
// pane is shown.
|
||||
// Note that is the screen is big enough so that drawers are not shown,
|
||||
// the state is "open" anyway, this is fine.
|
||||
setPaneDrawerState('right', 'open');
|
||||
}
|
||||
},
|
||||
[setPaneDrawerState, hasEditorsInRightPane]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (hasEditorsInLeftPane) {
|
||||
// Just opened the first editor in the left pane: ensure the drawer
|
||||
// pane is shown.
|
||||
// Note that is the screen is big enough so that drawers are not shown,
|
||||
// the state is "open" anyway, this is fine.
|
||||
setPaneDrawerState('left', 'open');
|
||||
}
|
||||
},
|
||||
[setPaneDrawerState, hasEditorsInLeftPane]
|
||||
);
|
||||
|
||||
const onCloseLeftPane = React.useCallback(
|
||||
() => setPaneDrawerState('left', 'closed'),
|
||||
[setPaneDrawerState]
|
||||
);
|
||||
useSwipeableDrawer({
|
||||
enabled: areSidePanesDrawers,
|
||||
paneRef: leftPaneRef,
|
||||
direction: 'left',
|
||||
onClose: onCloseLeftPane,
|
||||
});
|
||||
|
||||
const onCloseRightPane = React.useCallback(
|
||||
() => setPaneDrawerState('right', 'closed'),
|
||||
[setPaneDrawerState]
|
||||
);
|
||||
useSwipeableDrawer({
|
||||
enabled: areSidePanesDrawers,
|
||||
paneRef: rightPaneRef,
|
||||
direction: 'left',
|
||||
onClose: onCloseRightPane,
|
||||
});
|
||||
|
||||
const draggingStateRef = React.useRef<DraggingState | null>(null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const leftPane = leftPaneRef.current;
|
||||
const rightPane = rightPaneRef.current;
|
||||
const draggingState = draggingStateRef.current;
|
||||
if (!draggingState || !containerRef.current || !leftPane || !rightPane)
|
||||
return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const newWidth =
|
||||
draggingState.paneIdentifier === 'left'
|
||||
? draggingState.startWidth +
|
||||
(event.clientX - draggingState.startClientX)
|
||||
: draggingState.startWidth -
|
||||
(event.clientX - draggingState.startClientX);
|
||||
|
||||
const min = paneWidthMin;
|
||||
const max = containerRect.width - paneWidthMin;
|
||||
const clampedWidth = Math.max(min, Math.min(max, newWidth));
|
||||
|
||||
if (draggingState.paneIdentifier === 'left') {
|
||||
leftPane.style.flexBasis = `${clampedWidth}px`;
|
||||
} else {
|
||||
rightPane.style.flexBasis = `${clampedWidth}px`;
|
||||
}
|
||||
|
||||
// Only trigger a React re-render after the user has stopped dragging,
|
||||
// to avoid re-rendering the panes too often.
|
||||
debouncedForceUpdate();
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
draggingStateRef.current = null;
|
||||
forceUpdate();
|
||||
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
};
|
||||
|
||||
const onLeftResizerPointerDown = (event: PointerEvent) => {
|
||||
event.preventDefault();
|
||||
const leftPane = leftPaneRef.current;
|
||||
if (!leftPane) return;
|
||||
|
||||
draggingStateRef.current = {
|
||||
paneIdentifier: 'left',
|
||||
startClientX: event.clientX,
|
||||
startWidth: leftPane.getBoundingClientRect().width,
|
||||
};
|
||||
forceUpdate();
|
||||
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
};
|
||||
|
||||
const onRightResizerPointerDown = (event: PointerEvent) => {
|
||||
event.preventDefault();
|
||||
const rightPane = rightPaneRef.current;
|
||||
if (!rightPane) return;
|
||||
|
||||
draggingStateRef.current = {
|
||||
paneIdentifier: 'right',
|
||||
startClientX: event.clientX,
|
||||
startWidth: rightPane.getBoundingClientRect().width,
|
||||
};
|
||||
forceUpdate();
|
||||
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
};
|
||||
|
||||
const leftResizer = leftResizerRef.current;
|
||||
if (leftResizer) {
|
||||
leftResizer.addEventListener('pointerdown', onLeftResizerPointerDown);
|
||||
}
|
||||
|
||||
const rightResizer = rightResizerRef.current;
|
||||
if (rightResizer) {
|
||||
rightResizer.addEventListener('pointerdown', onRightResizerPointerDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (leftResizer) {
|
||||
leftResizer.removeEventListener(
|
||||
'pointerdown',
|
||||
onLeftResizerPointerDown
|
||||
);
|
||||
}
|
||||
if (rightResizer) {
|
||||
rightResizer.removeEventListener(
|
||||
'pointerdown',
|
||||
onRightResizerPointerDown
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
[debouncedForceUpdate, forceUpdate]
|
||||
);
|
||||
|
||||
const [
|
||||
leftPanePointerEventsNone,
|
||||
setLeftPanePointerEventsNone,
|
||||
] = React.useState(false);
|
||||
const [
|
||||
centerPanePointerEventsNone,
|
||||
setCenterPanePointerEventsNone,
|
||||
] = React.useState(false);
|
||||
const [
|
||||
rightPanePointerEventsNone,
|
||||
setRightPanePointerEventsNone,
|
||||
] = React.useState(false);
|
||||
|
||||
const isDragging = draggingStateRef.current !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classes.container}
|
||||
role="group"
|
||||
aria-label="Resizable split pane"
|
||||
style={isDragging ? styles.pointerEventsAll : undefined}
|
||||
>
|
||||
<div
|
||||
ref={leftPaneRef}
|
||||
className={classNames({
|
||||
[classes.pane]: true,
|
||||
[classes.leftPane]: true,
|
||||
[classes.drawer]: areSidePanesDrawers,
|
||||
[classes.closedDrawer]:
|
||||
areSidePanesDrawers && panesDrawerState['left'] === 'closed',
|
||||
[classes.hidden]: !hasEditorsInLeftPane,
|
||||
})}
|
||||
style={
|
||||
leftPanePointerEventsNone && !isDragging
|
||||
? styles.pointerEventsNone
|
||||
: undefined
|
||||
}
|
||||
id="pane-left"
|
||||
>
|
||||
{renderPane({
|
||||
paneIdentifier: 'left',
|
||||
isLeftMostPane: true,
|
||||
isRightMostPane: false,
|
||||
isDrawer: areSidePanesDrawers,
|
||||
areSidePanesDrawers,
|
||||
onSetPaneDrawerState: setPaneDrawerState,
|
||||
onSetPointerEventsNone: setLeftPanePointerEventsNone,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.resizer]: true,
|
||||
[classes.hidden]: !hasEditorsInLeftPane || areSidePanesDrawers,
|
||||
})}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-controls="pane-left pane-center"
|
||||
tabIndex={0}
|
||||
ref={leftResizerRef}
|
||||
/>
|
||||
<div
|
||||
ref={centerPaneRef}
|
||||
className={classNames({
|
||||
[classes.pane]: true,
|
||||
[classes.centerPane]: true,
|
||||
})}
|
||||
style={
|
||||
centerPanePointerEventsNone && !isDragging
|
||||
? styles.pointerEventsNone
|
||||
: undefined
|
||||
}
|
||||
id="pane-center"
|
||||
>
|
||||
{renderPane({
|
||||
paneIdentifier: 'center',
|
||||
isLeftMostPane: areSidePanesDrawers || !hasEditorsInLeftPane,
|
||||
isRightMostPane: areSidePanesDrawers || !hasEditorsInRightPane,
|
||||
isDrawer: false,
|
||||
areSidePanesDrawers,
|
||||
onSetPaneDrawerState: setPaneDrawerState,
|
||||
onSetPointerEventsNone: setCenterPanePointerEventsNone,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.resizer]: true,
|
||||
[classes.hidden]: !hasEditorsInRightPane || areSidePanesDrawers,
|
||||
})}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-controls="pane-center pane-right"
|
||||
tabIndex={0}
|
||||
ref={rightResizerRef}
|
||||
/>
|
||||
<div
|
||||
ref={rightPaneRef}
|
||||
className={classNames({
|
||||
[classes.pane]: true,
|
||||
[classes.rightPane]: true,
|
||||
[classes.drawer]: areSidePanesDrawers,
|
||||
[classes.closedDrawer]:
|
||||
areSidePanesDrawers && panesDrawerState['right'] === 'closed',
|
||||
[classes.hidden]: !hasEditorsInRightPane,
|
||||
})}
|
||||
style={
|
||||
rightPanePointerEventsNone && !isDragging
|
||||
? styles.pointerEventsNone
|
||||
: undefined
|
||||
}
|
||||
id="pane-right"
|
||||
>
|
||||
{renderPane({
|
||||
paneIdentifier: 'right',
|
||||
isLeftMostPane: false,
|
||||
isRightMostPane: true,
|
||||
isDrawer: areSidePanesDrawers,
|
||||
areSidePanesDrawers,
|
||||
onSetPaneDrawerState: setPaneDrawerState,
|
||||
onSetPointerEventsNone: setRightPanePointerEventsNone,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -53,13 +53,15 @@ const ProjectTitlebar = React.memo<Props>(
|
||||
.join(' - ');
|
||||
|
||||
Window.setTitle(title);
|
||||
Window.setTitleBarColor(gdevelopTheme.titlebar.backgroundColor);
|
||||
Window.setWindowBackgroundColor(
|
||||
gdevelopTheme.surface.window.backgroundColor
|
||||
);
|
||||
},
|
||||
[
|
||||
projectIdentifier,
|
||||
suffix,
|
||||
hasUnsavedChanges,
|
||||
gdevelopTheme.titlebar.backgroundColor,
|
||||
gdevelopTheme.surface.window.backgroundColor,
|
||||
storageProviderName,
|
||||
]
|
||||
);
|
||||
|
@@ -42,6 +42,7 @@ import { ProductLicenseStoreStateProvider } from '../AssetStore/ProductLicense/P
|
||||
import { MarketingPlansStoreStateProvider } from '../MarketingPlans/MarketingPlansStoreContext';
|
||||
import { CourseStoreStateProvider } from '../Course/CourseStoreContext';
|
||||
import { Resource3DPreviewProvider } from '../ResourcesList/ResourcePreview/Resource3DPreviewContext';
|
||||
import { AiRequestProvider } from '../AiGeneration/AiRequestContext';
|
||||
|
||||
type Props = {|
|
||||
authentication: Authentication,
|
||||
@@ -121,11 +122,13 @@ const Providers = ({
|
||||
<AnnouncementsFeedStateProvider>
|
||||
<PrivateAssetsAuthorizationProvider>
|
||||
<Resource3DPreviewProvider>
|
||||
{children(
|
||||
{
|
||||
i18n,
|
||||
}
|
||||
)}
|
||||
<AiRequestProvider>
|
||||
{children(
|
||||
{
|
||||
i18n,
|
||||
}
|
||||
)}
|
||||
</AiRequestProvider>
|
||||
</Resource3DPreviewProvider>
|
||||
</PrivateAssetsAuthorizationProvider>
|
||||
</AnnouncementsFeedStateProvider>
|
||||
|
@@ -3,8 +3,6 @@ import * as React from 'react';
|
||||
|
||||
import MenuIcon from '../UI/CustomSvgIcons/Menu';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import Window from '../Utils/Window';
|
||||
import {
|
||||
TitleBarLeftSafeMargins,
|
||||
TitleBarRightSafeMargins,
|
||||
@@ -39,6 +37,7 @@ const styles = {
|
||||
height: 34,
|
||||
},
|
||||
askAiContainer: {
|
||||
zIndex: 0, // Create a stacking context to avoid the AI icon z-indexed element to display above other panes or UI elements.
|
||||
marginBottom: 4,
|
||||
marginRight: 1,
|
||||
marginLeft: 2,
|
||||
@@ -52,8 +51,12 @@ type TabsTitlebarProps = {|
|
||||
onEditorTabHovered: (?EditorTab, {| isLabelTruncated: boolean |}) => void,
|
||||
onEditorTabClosing: () => void
|
||||
) => React.Node,
|
||||
hasAskAiOpened: boolean,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
isLeftMostPane: boolean,
|
||||
isRightMostPane: boolean,
|
||||
displayMenuIcon: boolean,
|
||||
|
||||
displayAskAi: boolean,
|
||||
onAskAiClicked: () => void,
|
||||
|};
|
||||
|
||||
const useIsAskAiIconAnimated = (shouldDisplayAskAi: boolean) => {
|
||||
@@ -103,12 +106,13 @@ export default function TabsTitlebar({
|
||||
toggleProjectManager,
|
||||
hidden,
|
||||
renderTabs,
|
||||
hasAskAiOpened,
|
||||
onOpenAskAi,
|
||||
isLeftMostPane,
|
||||
isRightMostPane,
|
||||
displayMenuIcon,
|
||||
displayAskAi,
|
||||
onAskAiClicked,
|
||||
}: TabsTitlebarProps) {
|
||||
const isTouchscreen = useScreenType() === 'touch';
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const backgroundColor = gdevelopTheme.titlebar.backgroundColor;
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const [tooltipData, setTooltipData] = React.useState<?{|
|
||||
@@ -117,13 +121,6 @@ export default function TabsTitlebar({
|
||||
|}>(null);
|
||||
const tooltipTimeoutId = React.useRef<?TimeoutID>(null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
Window.setTitleBarColor(backgroundColor);
|
||||
},
|
||||
[backgroundColor]
|
||||
);
|
||||
|
||||
const onEditorTabHovered = React.useCallback(
|
||||
(
|
||||
editorTab: ?EditorTab,
|
||||
@@ -189,35 +186,36 @@ export default function TabsTitlebar({
|
||||
limits.capabilities.classrooms.hideAskAi;
|
||||
|
||||
const shouldDisplayAskAi =
|
||||
preferences.values.showAiAskButtonInTitleBar &&
|
||||
!hasAskAiOpened &&
|
||||
!hideAskAi;
|
||||
preferences.values.showAiAskButtonInTitleBar && displayAskAi && !hideAskAi;
|
||||
const isAskAiIconAnimated = useIsAskAiIconAnimated(shouldDisplayAskAi);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...styles.container,
|
||||
backgroundColor,
|
||||
backgroundColor: 'transparent',
|
||||
// Hiding the titlebar should still keep its position in the layout to avoid layout shifts:
|
||||
visibility: hidden ? 'hidden' : 'visible',
|
||||
pointerEvents: hidden ? undefined : 'all',
|
||||
}}
|
||||
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>
|
||||
{isLeftMostPane && <TitleBarLeftSafeMargins />}
|
||||
{displayMenuIcon && (
|
||||
<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
|
||||
@@ -227,11 +225,11 @@ export default function TabsTitlebar({
|
||||
<TextButton
|
||||
icon={<RobotIcon size={16} rotating={isAskAiIconAnimated} />}
|
||||
label={'Ask AI'}
|
||||
onClick={() => onOpenAskAi('agent')}
|
||||
onClick={onAskAiClicked}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<TitleBarRightSafeMargins />
|
||||
{isRightMostPane && <TitleBarRightSafeMargins />}
|
||||
{tooltipData && (
|
||||
<TabsTitlebarTooltip
|
||||
anchorElement={tooltipData.element}
|
||||
|
@@ -30,7 +30,11 @@ type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<void>,
|
||||
openAskAi: (mode: 'chat' | 'agent') => void,
|
||||
openAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
storageProviders: Array<StorageProvider>,
|
||||
|};
|
||||
|
||||
@@ -179,7 +183,11 @@ const useNewProjectDialog = ({
|
||||
const onOpenAskAi = React.useCallback(
|
||||
() => {
|
||||
closeNewProjectDialog();
|
||||
openAskAi('agent');
|
||||
openAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
});
|
||||
},
|
||||
[closeNewProjectDialog, openAskAi]
|
||||
);
|
||||
|
@@ -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,13 @@ 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 +36,14 @@ import {
|
||||
closeEventsBasedObjectVariantTab,
|
||||
saveUiSettings,
|
||||
type EditorTabsState,
|
||||
type EditorTab,
|
||||
type EditorKind,
|
||||
getEventsFunctionsExtensionEditor,
|
||||
notifyPreviewOrExportWillStart,
|
||||
moveTabToTheRightOfHoveredTab,
|
||||
getCustomObjectEditor,
|
||||
hasEditorTabOpenedWithKey,
|
||||
getOpenedAskAiEditor,
|
||||
changeCurrentTab,
|
||||
getAllEditorTabs,
|
||||
hasEditorsInPane,
|
||||
} from './EditorTabs/EditorTabsHandler';
|
||||
import { renderDebuggerEditorContainer } from './EditorContainers/DebuggerEditorContainer';
|
||||
import { renderEventsEditorContainer } from './EditorContainers/EventsEditorContainer';
|
||||
@@ -65,9 +59,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 +123,6 @@ import {
|
||||
CommandPaletteWithAlgoliaSearch,
|
||||
type CommandPaletteInterface,
|
||||
} from '../CommandPalette/CommandPalette';
|
||||
import CommandsContextScopedProvider from '../CommandPalette/CommandsScopedContext';
|
||||
import { isExtensionNameTaken } from '../ProjectManager/EventFunctionExtensionNameVerifier';
|
||||
import {
|
||||
type PreviewState,
|
||||
@@ -174,7 +164,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 +197,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,11 +344,6 @@ const MainFrame = (props: Props) => {
|
||||
gdjsDevelopmentWatcherEnabled: false,
|
||||
}: State)
|
||||
);
|
||||
const toolbar = React.useRef<?ToolbarInterface>(null);
|
||||
const [
|
||||
tabsTitleBarAndEditorToolbarHidden,
|
||||
setTabsTitleBarAndEditorToolbarHidden,
|
||||
] = React.useState(false);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const [
|
||||
cloudProjectFileMetadataToRecover,
|
||||
@@ -583,13 +568,17 @@ const MainFrame = (props: Props) => {
|
||||
name,
|
||||
dontFocusTab,
|
||||
project,
|
||||
paneIdentifier,
|
||||
mode,
|
||||
aiRequestId,
|
||||
}: {
|
||||
kind: EditorKind,
|
||||
name: string,
|
||||
dontFocusTab?: boolean,
|
||||
project?: ?gdProject,
|
||||
paneIdentifier?: 'left' | 'center' | 'right' | null,
|
||||
mode?: 'chat' | 'agent',
|
||||
aiRequestId?: string | null,
|
||||
}) => {
|
||||
const label =
|
||||
kind === 'resources'
|
||||
@@ -663,7 +652,7 @@ const MainFrame = (props: Props) => {
|
||||
kind === 'start page'
|
||||
? { storageProviders: props.storageProviders }
|
||||
: kind === 'ask-ai'
|
||||
? { mode }
|
||||
? { mode, aiRequestId }
|
||||
: undefined;
|
||||
return {
|
||||
icon,
|
||||
@@ -684,6 +673,7 @@ const MainFrame = (props: Props) => {
|
||||
extraEditorProps,
|
||||
key,
|
||||
dontFocusTab,
|
||||
paneIdentifier: paneIdentifier || 'center',
|
||||
};
|
||||
},
|
||||
[i18n, props.storageProviders]
|
||||
@@ -777,26 +767,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.
|
||||
@@ -1233,20 +1203,48 @@ const MainFrame = (props: Props) => {
|
||||
});
|
||||
|
||||
const openAskAi = React.useCallback(
|
||||
(mode: 'chat' | 'agent') => {
|
||||
({
|
||||
mode,
|
||||
aiRequestId,
|
||||
paneIdentifier,
|
||||
}: {|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => {
|
||||
setState(state => {
|
||||
const askAiEditor = getOpenedAskAiEditor(state.editorTabs);
|
||||
if (askAiEditor) {
|
||||
askAiEditor.startNewChat(mode);
|
||||
const openedEditor = getOpenedAskAiEditor(state.editorTabs);
|
||||
let newEditorTabs = state.editorTabs;
|
||||
if (openedEditor) {
|
||||
if (
|
||||
!paneIdentifier ||
|
||||
openedEditor.paneIdentifier === paneIdentifier
|
||||
) {
|
||||
// The editor is opened, and at the right position.
|
||||
openedEditor.askAiEditor.startOrOpenChat({ mode, aiRequestId });
|
||||
return state;
|
||||
}
|
||||
|
||||
// The editor is opened, but not in the right pane.
|
||||
// Close it and it will re-open in the right pane.
|
||||
newEditorTabs = closeEditorTab(newEditorTabs, openedEditor.editorTab);
|
||||
}
|
||||
|
||||
// Open or focus the AI editor.
|
||||
// Open, or focus if already opened, the editor.
|
||||
newEditorTabs = openEditorTab(
|
||||
newEditorTabs,
|
||||
getEditorOpeningOptions({
|
||||
kind: 'ask-ai',
|
||||
name: '',
|
||||
mode,
|
||||
aiRequestId,
|
||||
paneIdentifier,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
editorTabs: openEditorTab(
|
||||
state.editorTabs,
|
||||
getEditorOpeningOptions({ kind: 'ask-ai', name: '', mode })
|
||||
),
|
||||
editorTabs: newEditorTabs,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -1288,18 +1286,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;
|
||||
@@ -2172,7 +2163,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
|
||||
@@ -2209,7 +2204,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
|
||||
@@ -2253,7 +2252,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
|
||||
@@ -2286,7 +2289,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
|
||||
@@ -2378,7 +2385,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();
|
||||
@@ -2390,7 +2397,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);
|
||||
@@ -2402,7 +2409,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);
|
||||
@@ -2414,7 +2421,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);
|
||||
@@ -3253,62 +3260,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) {
|
||||
@@ -3809,6 +3760,16 @@ const MainFrame = (props: Props) => {
|
||||
isApplicationTopLevelMenu: false,
|
||||
hideAskAi,
|
||||
};
|
||||
const onOpenAskAiFromMainMenu = React.useCallback(
|
||||
() => {
|
||||
openAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: currentProject ? 'right' : 'center',
|
||||
});
|
||||
},
|
||||
[openAskAi, currentProject]
|
||||
);
|
||||
const mainMenuCallbacks = {
|
||||
onChooseProject: () => openOpenFromStorageProviderDialog(),
|
||||
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
|
||||
@@ -3827,7 +3788,7 @@ const MainFrame = (props: Props) => {
|
||||
onOpenPreferences: () => openPreferencesDialog(true),
|
||||
onOpenLanguage: () => openLanguageDialog(true),
|
||||
onOpenProfile: onOpenProfileDialog,
|
||||
onOpenAskAi: openAskAi,
|
||||
onOpenAskAi: onOpenAskAiFromMainMenu,
|
||||
setElectronUpdateStatus: setElectronUpdateStatus,
|
||||
};
|
||||
|
||||
@@ -3837,7 +3798,86 @@ 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,
|
||||
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,
|
||||
onOpenAskAi: 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 hasEditorsInLeftPane = hasEditorsInPane(state.editorTabs, 'left');
|
||||
const hasEditorsInRightPane = hasEditorsInPane(state.editorTabs, 'right');
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -3909,80 +3949,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()}
|
||||
@@ -3991,149 +3957,30 @@ 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,
|
||||
onOpenAskAi: openAskAi,
|
||||
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
|
||||
hasEditorsInLeftPane={hasEditorsInLeftPane}
|
||||
hasEditorsInRightPane={hasEditorsInRightPane}
|
||||
renderPane={({
|
||||
paneIdentifier,
|
||||
isLeftMostPane,
|
||||
isRightMostPane,
|
||||
isDrawer,
|
||||
areSidePanesDrawers,
|
||||
onSetPointerEventsNone,
|
||||
onSetPaneDrawerState,
|
||||
}) => (
|
||||
<EditorTabsPane
|
||||
{...editorTabsPaneProps}
|
||||
paneIdentifier={paneIdentifier}
|
||||
isLeftMostPane={isLeftMostPane}
|
||||
isRightMostPane={isRightMostPane}
|
||||
isDrawer={isDrawer}
|
||||
areSidePanesDrawers={areSidePanesDrawers}
|
||||
onSetPointerEventsNone={onSetPointerEventsNone}
|
||||
onSetPaneDrawerState={onSetPaneDrawerState}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</LeaderboardProvider>
|
||||
<CommandPaletteWithAlgoliaSearch ref={commandPaletteRef} />
|
||||
<LoaderModal
|
||||
|
@@ -3,7 +3,6 @@ import { Trans, t } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||
@@ -387,7 +386,7 @@ const EditProfileDialog = ({
|
||||
const canDelete = !actionInProgress;
|
||||
|
||||
const onDeleteAccount = React.useCallback(
|
||||
async (i18n: I18nType) => {
|
||||
async () => {
|
||||
if (!canDelete || isStudentAccount) {
|
||||
return;
|
||||
}
|
||||
|
@@ -108,7 +108,11 @@ type Props = {|
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType
|
||||
) => Promise<void>,
|
||||
onOpenAskAi: (mode: 'chat' | 'agent') => void,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
selectedExampleShortHeader: ?ExampleShortHeader,
|
||||
onSelectExampleShortHeader: (exampleShortHeader: ?ExampleShortHeader) => void,
|
||||
selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData,
|
||||
@@ -606,7 +610,13 @@ const NewProjectSetupDialog = ({
|
||||
label={<Trans>Try the AI agent</Trans>}
|
||||
rightIcon={<ArrowRight />}
|
||||
style={styles.tryAIAgentButton}
|
||||
onClick={() => onOpenAskAi('agent')}
|
||||
onClick={() =>
|
||||
onOpenAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
{isOnline ? (
|
||||
|
@@ -16,9 +16,7 @@ const useClasses = (rotating, size) =>
|
||||
'&::before': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
background: `conic-gradient(${
|
||||
theme.palette.type === 'light' ? '#f0f0f0' : '#262231'
|
||||
}, #b07bf7)`,
|
||||
background: `conic-gradient(from -18deg at 59.52% 50%, #FFBC57 0deg, #F6945B 157.5deg, #DFA9E4 257.88461208343506deg, rgba(201, 182, 252, 0.00) 360deg)`,
|
||||
width: 'calc(100% * 1.41421356237)',
|
||||
paddingBottom: 'calc(100% * 1.41421356237)',
|
||||
position: 'absolute',
|
||||
|
@@ -93,7 +93,7 @@ export const ClosableTabs = ({ hideLabels, renderTabs }: ClosableTabsProps) => {
|
||||
flexWrap: 'nowrap', // Single line of tab...
|
||||
overflowX: 'overlay', // ...scroll horizontally if needed
|
||||
overflowY: 'hidden', // ...never scroll vertically (useful on Safari)
|
||||
marginTop: 6,
|
||||
marginTop: 7, // Chosen to align with the size of controls in the titlebar.
|
||||
};
|
||||
|
||||
const onScroll = React.useCallback((event: WheelEvent) => {
|
||||
|
@@ -123,8 +123,8 @@
|
||||
transparent,
|
||||
transparent,
|
||||
transparent,
|
||||
#b07bf7,
|
||||
#d2b5ff,
|
||||
#f6945b,
|
||||
#dfa9e4,
|
||||
transparent,
|
||||
transparent,
|
||||
transparent,
|
||||
|
17
newIDE/app/src/UI/CustomSvgIcons/ChatBubbles.js
Normal file
17
newIDE/app/src/UI/CustomSvgIcons/ChatBubbles.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
|
||||
export default React.memo(props => (
|
||||
<SvgIcon {...props} width="16" height="17" viewBox="0 0 16 17" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.00016 7.33337C7.30137 7.33337 9.16683 9.19883 9.16683 11.5C9.16683 13.8013 7.30137 15.6667 5.00016 15.6667C4.31044 15.6667 3.65935 15.4974 3.08545 15.1999L1.60824 15.4747C1.44622 15.5048 1.2801 15.4532 1.16357 15.3366C1.04704 15.2201 0.995409 15.054 1.02555 14.892L1.30029 13.4141C1.00294 12.8403 0.833496 12.1896 0.833496 11.5C0.833496 9.19883 2.69898 7.33337 5.00016 7.33337ZM5.00016 8.33337C3.25126 8.33337 1.8335 9.75112 1.8335 11.5C1.8335 12.0775 1.98763 12.6175 2.25667 13.0827C2.31643 13.186 2.33711 13.3079 2.31527 13.4252L2.14144 14.3581L3.07503 14.1849L3.16357 14.1765C3.25206 14.1759 3.33997 14.1987 3.41748 14.2435C3.88267 14.5126 4.42265 14.6667 5.00016 14.6667C6.74909 14.6667 8.16683 13.249 8.16683 11.5C8.16683 9.75112 6.74909 8.33337 5.00016 8.33337Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.3335 1.33337C12.5551 1.33344 15.1668 3.94509 15.1668 7.16671C15.1668 8.16108 14.917 9.0984 14.4774 9.91866L14.8914 12.142C14.9216 12.3039 14.8699 12.4701 14.7534 12.5866C14.6369 12.7031 14.4707 12.7547 14.3088 12.7247L12.0835 12.3106C11.5262 12.6091 10.915 12.8215 10.2677 12.9258C9.99532 12.9696 9.73889 12.7841 9.69482 12.5118C9.65092 12.2392 9.83633 11.9822 10.1089 11.9382C10.6982 11.8432 11.2518 11.6415 11.7495 11.3536C11.8527 11.2939 11.9741 11.2732 12.0913 11.295L13.7749 11.6081L13.4618 9.92517C13.4399 9.80787 13.4606 9.68601 13.5203 9.58272C13.9312 8.87235 14.1668 8.04778 14.1668 7.16671C14.1668 4.49737 12.0028 2.33344 9.3335 2.33337C6.90077 2.33343 4.88676 4.1314 4.54964 6.47074C4.51017 6.74396 4.25649 6.9333 3.98324 6.89392C3.71023 6.85433 3.52082 6.60126 3.56006 6.32817C3.96708 3.50378 6.39649 1.33343 9.3335 1.33337Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
));
|
@@ -5,6 +5,7 @@ import MuiDialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import {
|
||||
TopLevelWindowSizeProvider,
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from './Responsive/ResponsiveWindowMeasurer';
|
||||
@@ -480,4 +481,12 @@ const Dialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
const DialogWithTopLevelWindowSize = (props: DialogProps) => {
|
||||
return (
|
||||
<TopLevelWindowSizeProvider>
|
||||
<Dialog {...props} />
|
||||
</TopLevelWindowSizeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogWithTopLevelWindowSize;
|
||||
|
@@ -8,7 +8,10 @@ import Cross from './CustomSvgIcons/Cross';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { tooltipEnterDelay } from './Tooltip';
|
||||
import { LineStackLayout } from './Layout';
|
||||
import { TitleBarLeftSafeMargins } from './TitleBarSafeMargins';
|
||||
import {
|
||||
TitleBarLeftSafeMargins,
|
||||
TitleBarRightSafeMargins,
|
||||
} from './TitleBarSafeMargins';
|
||||
|
||||
const appBarHeight = 38;
|
||||
|
||||
@@ -37,12 +40,24 @@ const styles = {
|
||||
|
||||
type Props = {|
|
||||
icon?: React.Node,
|
||||
drawerAnchor?: 'left' | 'right',
|
||||
title: React.Node,
|
||||
onClose: () => void,
|
||||
id: string,
|
||||
|};
|
||||
|
||||
const DrawerTopBar = (props: Props) => {
|
||||
const closeButton = (
|
||||
<IconButton
|
||||
onClick={props.onClose}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
size="small"
|
||||
id={`${props.id}-close`}
|
||||
>
|
||||
<Cross />
|
||||
</IconButton>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<AppBar
|
||||
@@ -53,7 +68,7 @@ const DrawerTopBar = (props: Props) => {
|
||||
elevation={0}
|
||||
>
|
||||
<Toolbar style={styles.toolbar}>
|
||||
<TitleBarLeftSafeMargins />
|
||||
{props.drawerAnchor !== 'right' && <TitleBarLeftSafeMargins />}
|
||||
<LineStackLayout noMargin expand alignItems="center">
|
||||
{props.icon && (
|
||||
<IconButton
|
||||
@@ -82,15 +97,8 @@ const DrawerTopBar = (props: Props) => {
|
||||
</Typography>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<IconButton
|
||||
onClick={props.onClose}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
size="small"
|
||||
id={`${props.id}-close`}
|
||||
>
|
||||
<Cross />
|
||||
</IconButton>
|
||||
{closeButton}
|
||||
{props.drawerAnchor === 'right' && <TitleBarRightSafeMargins />}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import useOnResize from '../../Utils/UseOnResize';
|
||||
|
||||
// Typically, small corresponds to mobile phones.
|
||||
@@ -8,6 +7,23 @@ import useOnResize from '../../Utils/UseOnResize';
|
||||
// Large corresponds to most laptop and desktop screens.
|
||||
// Xlarge corresponds to large desktop screens.
|
||||
export type WindowSizeType = 'small' | 'medium' | 'large' | 'xlarge';
|
||||
export type OrientationAndWindowSizeType =
|
||||
| 'small-portrait'
|
||||
| 'medium-portrait'
|
||||
| 'large-portrait'
|
||||
| 'xlarge-portrait'
|
||||
| 'small-landscape'
|
||||
| 'medium-landscape'
|
||||
| 'large-landscape'
|
||||
| 'xlarge-landscape';
|
||||
|
||||
type WindowSize = {|
|
||||
windowSize: WindowSizeType,
|
||||
isMobile: boolean,
|
||||
isMediumScreen: boolean,
|
||||
isLandscape: boolean,
|
||||
|};
|
||||
|
||||
const sizeThresholds = {
|
||||
smallHeight: 500,
|
||||
smallWidth: 600,
|
||||
@@ -15,7 +31,156 @@ const sizeThresholds = {
|
||||
largeWidth: 1800,
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
/**
|
||||
* Give the orientation and window size from the specified dimensions
|
||||
* (be it the top-level window or a specific size, like a tab of the editor).
|
||||
*/
|
||||
const getOrientationAndWindowSizeFromDimensions = (
|
||||
innerWidth: number,
|
||||
innerHeight: number
|
||||
): OrientationAndWindowSizeType => {
|
||||
const orientation = innerWidth > innerHeight ? 'landscape' : 'portrait';
|
||||
|
||||
const sizeType =
|
||||
innerWidth < sizeThresholds.smallWidth ||
|
||||
innerHeight < sizeThresholds.smallHeight // Mobile devices can be in landscape mode, so check both width and height.
|
||||
? 'small'
|
||||
: innerWidth < sizeThresholds.mediumWidth
|
||||
? 'medium'
|
||||
: innerWidth < sizeThresholds.largeWidth
|
||||
? 'large'
|
||||
: 'xlarge';
|
||||
|
||||
// $FlowFixMe - this is guaranteed to be a valid OrientationAndWindowSizeType.
|
||||
return sizeType + '-' + orientation;
|
||||
};
|
||||
|
||||
// Map from orientation and window size to a WindowSize object, to ensure stability
|
||||
// across re-renders.
|
||||
const keyToSize: { [OrientationAndWindowSizeType]: WindowSize } = {
|
||||
'small-portrait': {
|
||||
windowSize: 'small',
|
||||
isMobile: true,
|
||||
isMediumScreen: false,
|
||||
isLandscape: false,
|
||||
},
|
||||
'medium-portrait': {
|
||||
windowSize: 'medium',
|
||||
isMobile: false,
|
||||
isMediumScreen: true,
|
||||
isLandscape: false,
|
||||
},
|
||||
'large-portrait': {
|
||||
windowSize: 'large',
|
||||
isMobile: false,
|
||||
isMediumScreen: false,
|
||||
isLandscape: false,
|
||||
},
|
||||
'xlarge-portrait': {
|
||||
windowSize: 'xlarge',
|
||||
isMobile: false,
|
||||
isMediumScreen: false,
|
||||
isLandscape: false,
|
||||
},
|
||||
'small-landscape': {
|
||||
windowSize: 'small',
|
||||
isMobile: true,
|
||||
isMediumScreen: false,
|
||||
isLandscape: true,
|
||||
},
|
||||
'medium-landscape': {
|
||||
windowSize: 'medium',
|
||||
isMobile: false,
|
||||
isMediumScreen: true,
|
||||
isLandscape: true,
|
||||
},
|
||||
'large-landscape': {
|
||||
windowSize: 'large',
|
||||
isMobile: false,
|
||||
isMediumScreen: false,
|
||||
isLandscape: true,
|
||||
},
|
||||
'xlarge-landscape': {
|
||||
windowSize: 'xlarge',
|
||||
isMobile: false,
|
||||
isMediumScreen: false,
|
||||
isLandscape: true,
|
||||
},
|
||||
};
|
||||
|
||||
const getWindowSizeFromTopLevelWindow = (): WindowSize | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return keyToSize[
|
||||
getOrientationAndWindowSizeFromDimensions(
|
||||
window.innerWidth,
|
||||
window.innerHeight
|
||||
)
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* A context that has a specific window size. If null, the window size is
|
||||
* supposed to be determined by the top-level window itself.
|
||||
*/
|
||||
const WindowSizeContext = React.createContext<WindowSize | null>(null);
|
||||
|
||||
type TopLevelWindowSizeProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
/**
|
||||
* "Reset" the dimensions to use the size of the window itself.
|
||||
* Typically: for dialogs.
|
||||
*/
|
||||
export const TopLevelWindowSizeProvider = ({
|
||||
children,
|
||||
}: TopLevelWindowSizeProviderProps) => {
|
||||
return (
|
||||
<WindowSizeContext.Provider value={null}>
|
||||
{children}
|
||||
</WindowSizeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type SpecificDimensionsWindowSizeProviderProps = {|
|
||||
children: React.Node,
|
||||
innerWidth: number | null,
|
||||
innerHeight: number | null,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Pass a specific dimension for the children to use.
|
||||
* Useful when the component knows the dimensions in which its children are displayed.
|
||||
* Typically: a tab in an editor.
|
||||
*/
|
||||
export const SpecificDimensionsWindowSizeProvider = ({
|
||||
children,
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
}: SpecificDimensionsWindowSizeProviderProps) => {
|
||||
const windowSize = React.useMemo(
|
||||
() => {
|
||||
if (innerWidth === null || innerHeight === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return keyToSize[
|
||||
getOrientationAndWindowSizeFromDimensions(innerWidth, innerHeight)
|
||||
];
|
||||
},
|
||||
[innerWidth, innerHeight]
|
||||
);
|
||||
|
||||
return (
|
||||
<WindowSizeContext.Provider value={windowSize}>
|
||||
{windowSize ? children : null}
|
||||
</WindowSizeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type ResponsiveWindowMeasurerProps = {|
|
||||
children: ({
|
||||
windowSize: WindowSizeType,
|
||||
isMobile: boolean,
|
||||
@@ -27,59 +192,40 @@ type Props = {|
|
||||
/**
|
||||
* Wraps useResponsiveWindowSize in a component.
|
||||
*/
|
||||
export const ResponsiveWindowMeasurer = ({ children }: Props) =>
|
||||
children(useResponsiveWindowSize());
|
||||
export const ResponsiveWindowMeasurer = ({
|
||||
children,
|
||||
}: ResponsiveWindowMeasurerProps) => children(useResponsiveWindowSize());
|
||||
|
||||
/**
|
||||
* Return the size of the window.
|
||||
* This considers a window to be "small" if *both* the width and height
|
||||
* are small.
|
||||
* The returned object is stable as long as the window size has not changed from
|
||||
* one category to another.
|
||||
*/
|
||||
export const useResponsiveWindowSize = (): {
|
||||
windowSize: WindowSizeType,
|
||||
isMobile: boolean,
|
||||
isMediumScreen: boolean,
|
||||
isLandscape: boolean,
|
||||
} => {
|
||||
useOnResize(useForceUpdate());
|
||||
export const useResponsiveWindowSize = (): WindowSize => {
|
||||
const windowSizeFromContext = React.useContext(WindowSizeContext);
|
||||
const [windowSizeFromWindow, setWindowSizeFromWindow] = React.useState(
|
||||
getWindowSizeFromTopLevelWindow
|
||||
);
|
||||
useOnResize(
|
||||
React.useCallback(
|
||||
() => {
|
||||
if (windowSizeFromContext) {
|
||||
return; // Size is defined by the context already.
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
windowSize: 'medium',
|
||||
isMobile: false,
|
||||
isMediumScreen: true,
|
||||
isLandscape: true,
|
||||
};
|
||||
}
|
||||
// Only trigger a re-render if the window size has changed.
|
||||
const newWindowSize = getWindowSizeFromTopLevelWindow();
|
||||
if (newWindowSize !== windowSizeFromWindow) {
|
||||
setWindowSizeFromWindow(newWindowSize);
|
||||
}
|
||||
},
|
||||
[windowSizeFromWindow, windowSizeFromContext]
|
||||
)
|
||||
);
|
||||
|
||||
const isLandscape = window.innerWidth > window.innerHeight;
|
||||
|
||||
return window.innerWidth < sizeThresholds.smallWidth ||
|
||||
window.innerHeight < sizeThresholds.smallHeight // Mobile devices can be in landscape mode, so check both width and height.
|
||||
? {
|
||||
windowSize: 'small',
|
||||
isMobile: true,
|
||||
isMediumScreen: false,
|
||||
isLandscape,
|
||||
}
|
||||
: window.innerWidth < sizeThresholds.mediumWidth
|
||||
? {
|
||||
windowSize: 'medium',
|
||||
isMobile: false,
|
||||
isMediumScreen: true,
|
||||
isLandscape,
|
||||
}
|
||||
: window.innerWidth < sizeThresholds.largeWidth
|
||||
? {
|
||||
windowSize: 'large',
|
||||
isMobile: false,
|
||||
isMediumScreen: false,
|
||||
isLandscape,
|
||||
}
|
||||
: {
|
||||
windowSize: 'xlarge',
|
||||
isMobile: false,
|
||||
isMediumScreen: false,
|
||||
isLandscape,
|
||||
};
|
||||
return (
|
||||
windowSizeFromContext ||
|
||||
windowSizeFromWindow ||
|
||||
keyToSize['medium-landscape']
|
||||
);
|
||||
};
|
||||
|
@@ -6,7 +6,7 @@ import { type ButtonInterface } from './Button';
|
||||
|
||||
type Props = {|
|
||||
label: React.Node,
|
||||
onClick: ?(ev: any) => void | Promise<void>,
|
||||
onClick: ?() => void | Promise<void>,
|
||||
primary?: boolean,
|
||||
secondary?: boolean,
|
||||
allowBrowserAutoTranslate?: boolean,
|
||||
|
@@ -103,12 +103,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#25252E",
|
||||
@@ -117,7 +111,8 @@
|
||||
},
|
||||
"window": {
|
||||
"background-color": {
|
||||
"value": "#252525"
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
|
@@ -477,8 +477,10 @@ export function createGdevelopTheme({
|
||||
color: styles['ThemeMessageHotColor'],
|
||||
},
|
||||
},
|
||||
titlebar: {
|
||||
backgroundColor: styles['ThemeSurfaceTitlebarBackgroundColor'],
|
||||
surface: {
|
||||
window: {
|
||||
backgroundColor: styles['ThemeSurfaceWindowBackgroundColor'],
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
backgroundColor: styles['ThemeSurfaceToolbarBackgroundColor'],
|
||||
|
@@ -136,12 +136,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#25252E",
|
||||
|
@@ -136,12 +136,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#D9D9DE",
|
||||
"comment": "Palette/Grey/20"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#FAFAFA",
|
||||
@@ -150,7 +144,7 @@
|
||||
},
|
||||
"window": {
|
||||
"background-color": {
|
||||
"value": "#F7F7F7"
|
||||
"value": "#F5F5F7"
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
@@ -369,8 +363,8 @@
|
||||
"closable-tabs": {
|
||||
"default": {
|
||||
"background-color": {
|
||||
"value": "#A6A6AB",
|
||||
"comment": "Palette/Grey/40"
|
||||
"value": "#D9D9DE",
|
||||
"comment": "Background/tertiary"
|
||||
},
|
||||
"color": {
|
||||
"value": "#1D1D26",
|
||||
|
@@ -14,6 +14,36 @@ const jss = create({
|
||||
plugins: [...jssPreset().plugins, rtl()],
|
||||
});
|
||||
|
||||
type MuiThemeProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Allow to override the Material-UI theme for a specific subtree.
|
||||
* Useful for panes which are having a different "responsive window size"
|
||||
* than the rest of the application.
|
||||
*/
|
||||
export const MuiThemeOnlyProvider = ({ children }: MuiThemeProviderProps) => {
|
||||
const { values } = React.useContext(PreferencesContext);
|
||||
const { themeName, language } = values;
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
|
||||
const theme = React.useMemo(
|
||||
() => {
|
||||
const fullTheme = getFullTheme({
|
||||
themeName,
|
||||
language,
|
||||
isMobile,
|
||||
});
|
||||
|
||||
return fullTheme;
|
||||
},
|
||||
[themeName, language, isMobile]
|
||||
);
|
||||
|
||||
return <ThemeProvider theme={theme.muiTheme}>{children}</ThemeProvider>;
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
children: React.Node,
|
||||
forcedThemeName?: string,
|
||||
|
@@ -101,11 +101,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#2E3440"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#3B4252"
|
||||
|
@@ -101,12 +101,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#25252E",
|
||||
@@ -115,7 +109,8 @@
|
||||
},
|
||||
"window": {
|
||||
"background-color": {
|
||||
"value": "#21252B"
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
|
@@ -101,11 +101,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#191724"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#1f1d2e"
|
||||
|
@@ -101,12 +101,6 @@
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"titlebar": {
|
||||
"background-color": {
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#002B36"
|
||||
@@ -114,7 +108,8 @@
|
||||
},
|
||||
"window": {
|
||||
"background-color": {
|
||||
"value": "#00212B"
|
||||
"value": "#1D1D26",
|
||||
"comment": "Palette/Grey/100"
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
|
@@ -48,6 +48,7 @@ export const Toolbar = React.memo<ToolbarProps>(
|
||||
// Use content-visibility as we know the exact height of the toolbar, so the
|
||||
// content can be entirely skipped when hidden:
|
||||
contentVisibility: hidden ? 'hidden' : 'visible',
|
||||
pointerEvents: hidden ? undefined : 'all',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -8,7 +8,11 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
type Props = {|
|
||||
openInAppTutorialDialog: (tutorialId: string) => void,
|
||||
openProfileDialog: () => void,
|
||||
openAskAi: (mode: 'agent' | 'chat') => void,
|
||||
openAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -76,7 +80,11 @@ const useOpenInitialDialog = ({
|
||||
// in the manage tab. So the homepage handles the route arguments itself.
|
||||
break;
|
||||
case 'ask-ai':
|
||||
openAskAi('agent');
|
||||
openAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@@ -20,7 +20,7 @@ type YesNoCancelDialogChoice = 'yes' | 'no' | 'cancel';
|
||||
*/
|
||||
export const POSITIONAL_ARGUMENTS_KEY = '_';
|
||||
|
||||
let currentTitleBarColor: ?string = null;
|
||||
let currentWindowBackgroundColor: ?string = null;
|
||||
|
||||
const onChangeCallbacks = new Set<() => void>();
|
||||
let debouncedGeometryChange = null;
|
||||
@@ -94,8 +94,8 @@ export default class Window {
|
||||
}
|
||||
}
|
||||
|
||||
static setTitleBarColor(newColor: string) {
|
||||
if (currentTitleBarColor === newColor) {
|
||||
static setWindowBackgroundColor(newColor: string) {
|
||||
if (currentWindowBackgroundColor === newColor) {
|
||||
// Avoid potentially expensive DOM query/modification if no changes needed.
|
||||
return;
|
||||
}
|
||||
@@ -108,15 +108,25 @@ export default class Window {
|
||||
? '#000000'
|
||||
: '#ffffff',
|
||||
});
|
||||
currentTitleBarColor = newColor;
|
||||
}
|
||||
|
||||
// Update the PWA titlebar/controls color (if it's an installed PWA).
|
||||
const metaElement = document.querySelector('meta[name="theme-color"]');
|
||||
if (metaElement) {
|
||||
metaElement.setAttribute('content', newColor);
|
||||
currentTitleBarColor = newColor;
|
||||
}
|
||||
|
||||
// Update the window background color. Update both `body` and `html` elements
|
||||
// to ensure the background color is visible when resized.
|
||||
const body = document.body;
|
||||
if (body) {
|
||||
body.style.backgroundColor = newColor;
|
||||
}
|
||||
if (document.documentElement) {
|
||||
document.documentElement.style.backgroundColor = newColor;
|
||||
}
|
||||
|
||||
currentWindowBackgroundColor = newColor;
|
||||
}
|
||||
|
||||
static setBounds(x: number, y: number, width: number, height: number) {
|
||||
|
@@ -2,18 +2,22 @@
|
||||
import * as React from 'react';
|
||||
import { useStableUpToDateCallback } from './UseStableUpToDateCallback';
|
||||
|
||||
export const useTriggerAtNextRender = (callback: () => Promise<void>) => {
|
||||
export const useTriggerAtNextRender = <Args>(
|
||||
callback: (args: Args | null) => Promise<void>
|
||||
): ((args: Args) => void) => {
|
||||
const stableUpToDateCallback = useStableUpToDateCallback(callback);
|
||||
const [trigger, updateTrigger] = React.useState(0);
|
||||
const triggerAtNextRender = React.useCallback(() => {
|
||||
const [args, setArgs] = React.useState<Args | null>(null);
|
||||
const triggerAtNextRender = React.useCallback((args: Args) => {
|
||||
setArgs(args);
|
||||
updateTrigger(trigger => trigger + 1);
|
||||
}, []);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
stableUpToDateCallback();
|
||||
stableUpToDateCallback(args);
|
||||
},
|
||||
[trigger, stableUpToDateCallback]
|
||||
[trigger, args, stableUpToDateCallback]
|
||||
);
|
||||
return triggerAtNextRender;
|
||||
};
|
||||
|
@@ -41,7 +41,7 @@ try {
|
||||
language: 'en', // language is not important here as we only look for a color.
|
||||
isMobile: true, // window size is not important as we only look for a color.
|
||||
});
|
||||
color = theme.muiTheme.palette.background.default;
|
||||
color = theme.gdevelopTheme.surface.window.backgroundColor;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -69,7 +69,7 @@ type State = {|
|
||||
|
||||
class Bootstrapper extends Component<{}, State> {
|
||||
state = {
|
||||
loadingMessage: 'Loading the editor...',
|
||||
loadingMessage: '',
|
||||
App: null,
|
||||
};
|
||||
authentication = new Authentication();
|
||||
|
@@ -50,7 +50,7 @@ const commonProps = {
|
||||
onProcessFunctionCalls: async () => {},
|
||||
setAutoProcessFunctionCalls: () => {},
|
||||
isAutoProcessingFunctionCalls: false,
|
||||
onStartNewChat: () => {},
|
||||
onStartOrOpenChat: () => {},
|
||||
};
|
||||
|
||||
const fakeOutputWithUserRequestOnly = [
|
||||
|
@@ -46,7 +46,7 @@ const commonProps = {
|
||||
onProcessFunctionCalls: async () => {},
|
||||
setAutoProcessFunctionCalls: () => {},
|
||||
isAutoProcessingFunctionCalls: false,
|
||||
onStartNewChat: () => {},
|
||||
onStartOrOpenChat: () => {},
|
||||
};
|
||||
|
||||
export const NewAiRequest = () => (
|
||||
|
@@ -94,7 +94,7 @@ const WrappedHomePage = ({
|
||||
isActive={true}
|
||||
projectItemName={null}
|
||||
setToolbar={() => {}}
|
||||
hideTabsTitleBarAndEditorToolbar={() => {}}
|
||||
setGamesPlatformFrameShown={() => {}}
|
||||
canOpen={true}
|
||||
storageProviders={[CloudStorageProvider]}
|
||||
onChooseProject={() => action('onChooseProject')()}
|
||||
@@ -144,6 +144,7 @@ const WrappedHomePage = ({
|
||||
iframeLoaded: false,
|
||||
iframeVisible: false,
|
||||
iframeErrored: false,
|
||||
updateIframePosition: () => {},
|
||||
renderGamesPlatformFrame: () => null,
|
||||
}}
|
||||
onExtensionInstalled={action('onExtensionInstalled')}
|
||||
|
@@ -253,6 +253,8 @@ app.on('ready', function() {
|
||||
// setTitleBarOverlay seems not defined on macOS.
|
||||
if (mainWindow.setTitleBarOverlay)
|
||||
mainWindow.setTitleBarOverlay(overlayOptions);
|
||||
if (mainWindow.setBackgroundColor)
|
||||
mainWindow.setBackgroundColor(overlayOptions.color);
|
||||
}
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user