Merge remote-tracking branch 'official/master' into move-instance-in-game-2

This commit is contained in:
Davy Hélard
2025-06-17 10:54:35 +02:00
8 changed files with 355 additions and 242 deletions

View File

@@ -67,13 +67,15 @@ const ConditionsActionsColumns = (props: Props) => {
);
}
const conditionWidth =
getConditionWidthRatio(props.eventsSheetWidth) * props.eventsSheetWidth -
props.leftIndentWidth;
return (
<div style={styles.twoColumnsContainer} className={props.className}>
{props.renderConditionsList({
style: {
width: `${getConditionWidthRatio(props.eventsSheetWidth) *
props.eventsSheetWidth -
props.leftIndentWidth}px`,
width: `${conditionWidth}px`,
},
className: conditionsContainer,
})}

View File

@@ -91,7 +91,10 @@ import {
pasteInstructionsFromClipboardInInstructionsList,
} from './ClipboardKind';
import { useScreenType } from '../UI/Responsive/ScreenTypeMeasurer';
import { ResponsiveWindowMeasurer } from '../UI/Responsive/ResponsiveWindowMeasurer';
import {
type WindowSizeType,
useResponsiveWindowSize,
} from '../UI/Responsive/ResponsiveWindowMeasurer';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import AuthenticatedUserContext, {
type AuthenticatedUser,
@@ -156,6 +159,7 @@ type Props = {|
type ComponentProps = {|
...Props,
windowSize: WindowSizeType,
authenticatedUser: AuthenticatedUser,
preferences: Preferences,
tutorials: ?Array<Tutorial>,
@@ -236,6 +240,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
_eventSearcher: ?EventsSearcher;
_searchPanel: ?SearchPanelInterface;
_containerDiv = React.createRef<HTMLDivElement>();
_containerDivLastKnownSize = null;
_keyboardShortcuts = new KeyboardShortcuts({
isActive: () =>
!this.state.inlineEditing &&
@@ -1653,10 +1658,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
this.deleteSelection({ deleteInstructions: false });
};
_ensureEventUnfolded = (cb: () => ?gdBaseEvent) => {
_ensureUnfoldedAndScrollTo = (cb: () => ?gdBaseEvent) => {
const event = cb();
if (event && this._eventsTree) {
this._eventsTree.unfoldForEvent(event);
const eventsTree = this._eventsTree;
if (event && eventsTree) {
eventsTree.unfoldForEvent(event);
setTimeout(() => {
eventsTree.scrollToRow(eventsTree.getEventRow(event));
}, 0);
}
};
@@ -1837,6 +1846,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
onCreateEventsFunction,
tutorials,
hotReloadPreviewButtonProps,
windowSize,
} = this.props;
if (!project) return null;
@@ -1873,222 +1883,225 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
? this.state.editedParameter.eventContext.projectScopedContainersAccessor
: projectScopedContainersAccessor;
// Memorize the last size of the container div, that is used to render the events tree.
// When the events editor tab is hidden, the container div width/height are 0.
// In this case, we keep the previous known size to avoid trashing the layout and events tree
// memorized heights for events (which would create flickering when switching back
// to the events editor tab).
if (this._containerDiv.current) {
const width = this._containerDiv.current.clientWidth;
const height = this._containerDiv.current.clientHeight;
if (width > 0 && height > 0) {
this._containerDivLastKnownSize = {
width,
height,
};
}
}
return (
<>
<ResponsiveWindowMeasurer>
{({ windowSize }) => (
<EventsSearcher
key={events.ptr}
ref={eventSearcher => (this._eventSearcher = eventSearcher)}
events={events}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
selection={this.state.selection}
project={project}
<EventsSearcher
key={events.ptr}
ref={eventSearcher => (this._eventSearcher = eventSearcher)}
events={events}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
selection={this.state.selection}
project={project}
>
{({
eventsSearchResultEvents,
searchFocusOffset,
searchInEvents,
replaceInEvents,
goToPreviousSearchResult,
goToNextSearchResult,
}) => (
<div
id="events-editor"
data-active={isActive ? 'true' : undefined}
className="gd-events-sheet"
style={styles.container}
onKeyDown={this._keyboardShortcuts.onKeyDown}
onKeyUp={this._keyboardShortcuts.onKeyUp}
onDragOver={this._keyboardShortcuts.onDragOver}
ref={this._containerDiv}
tabIndex={0}
>
{({
eventsSearchResultEvents,
searchFocusOffset,
searchInEvents,
replaceInEvents,
goToPreviousSearchResult,
goToNextSearchResult,
}) => (
<div
id="events-editor"
data-active={isActive ? 'true' : undefined}
className="gd-events-sheet"
style={styles.container}
onKeyDown={this._keyboardShortcuts.onKeyDown}
onKeyUp={this._keyboardShortcuts.onKeyUp}
onDragOver={this._keyboardShortcuts.onDragOver}
ref={this._containerDiv}
tabIndex={0}
>
{isFunctionOnlyCallingItself && (
<Line>
<Column expand>
<AlertMessage kind="warning">
<Trans>
This function calls itself (it is "recursive").
Ensure this is expected and there is a proper
condition to stop it if necessary.
</Trans>
</AlertMessage>
</Column>
</Line>
)}
<EventsTree
ref={eventsTree => (this._eventsTree = eventsTree)}
key={events.ptr}
indentScale={preferences.values.eventsSheetIndentScale}
onScroll={this._ensureFocused}
events={events}
project={project}
scope={scope}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
projectScopedContainersAccessor={
projectScopedContainersAccessor
}
selection={this.state.selection}
onInstructionClick={this.selectInstruction}
onInstructionDoubleClick={this.openInstructionEditor}
onInstructionContextMenu={this.openInstructionContextMenu}
onAddInstructionContextMenu={
this.openAddInstructionContextMenu
}
onAddNewInstruction={this.openInstructionEditor}
onPasteInstructions={
this.pasteInstructionsInInstructionsList
}
onMoveToInstruction={this.moveSelectionToInstruction}
onMoveToInstructionsList={
this.moveSelectionToInstructionsList
}
onParameterClick={this.openParameterEditor}
onVariableDeclarationClick={() => {
// Nothing to do.
}}
onVariableDeclarationDoubleClick={this.openVariablesEditor}
onEventClick={this.selectEvent}
onEventContextMenu={this.openEventContextMenu}
onAddNewEvent={(
eventType: string,
eventsList: gdEventsList
) => {
this.addNewEvent(eventType, {
eventsList,
indexInList: eventsList.getEventsCount(),
});
}}
onOpenExternalEvents={onOpenExternalEvents}
onOpenLayout={onOpenLayout}
searchResults={eventsSearchResultEvents}
searchFocusOffset={searchFocusOffset}
onEventMoved={this._onEventMoved}
onEndEditingEvent={this._onEndEditingStringEvent}
showObjectThumbnails={
preferences.values.eventsSheetShowObjectThumbnails
}
screenType={screenType}
windowSize={windowSize}
eventsSheetWidth={
this._containerDiv.current
? this._containerDiv.current.clientWidth
: 0
}
eventsSheetHeight={
this._containerDiv.current
? this._containerDiv.current.clientHeight
: 0
}
fontSize={preferences.values.eventsSheetZoomLevel}
preferences={preferences}
tutorials={tutorials}
/>
{this.state.showSearchPanel && (
<ErrorBoundary
componentTitle={<Trans>Search panel</Trans>}
scope="scene-events-search"
onClose={() => this._closeSearchPanel()}
>
<SearchPanel
ref={searchPanel => (this._searchPanel = searchPanel)}
onSearchInEvents={inputs =>
this._searchInEvents(searchInEvents, inputs)
}
onReplaceInEvents={inputs => {
this._replaceInEvents(replaceInEvents, inputs);
}}
resultsCount={
eventsSearchResultEvents
? eventsSearchResultEvents.length
: null
}
hasEventSelected={hasEventSelected(
this.state.selection
)}
onGoToPreviousSearchResult={() =>
this._ensureEventUnfolded(goToPreviousSearchResult)
}
onCloseSearchPanel={() => {
this._closeSearchPanel();
}}
onGoToNextSearchResult={() =>
this._ensureEventUnfolded(goToNextSearchResult)
}
searchFocusOffset={searchFocusOffset}
/>
</ErrorBoundary>
)}
<InlineParameterEditor
open={this.state.inlineEditing}
anchorEl={this.state.inlineEditingAnchorEl}
onRequestClose={() => {
this.closeParameterEditor(
/*shouldCancel=*/ preferences.values
.eventsSheetCancelInlineParameter === 'cancel'
);
}}
onApply={() => {
this.closeParameterEditor(/*shouldCancel=*/ false);
}}
project={project}
scope={scope}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
projectScopedContainersAccessor={
editedParameterProjectScopedContainersAccessor
}
isCondition={this.state.editedParameter.isCondition}
instruction={this.state.editedParameter.instruction}
parameterIndex={this.state.editedParameter.parameterIndex}
onChange={value => {
const {
instruction,
parameterIndex,
} = this.state.editedParameter;
if (!instruction || !this.state.inlineEditing) {
// Unlikely to ever happen, but maybe a component could
// fire the "onChange" while the inline editor was just
// dismissed.
return;
}
instruction.setParameter(parameterIndex, value);
gd.VariableInstructionSwitcher.switchBetweenUnifiedInstructionIfNeeded(
project.getCurrentPlatform(),
editedParameterProjectScopedContainersAccessor.get(),
instruction
);
// Ask the component to re-render, so that the new parameter
// set for the instruction in the state
// is taken into account for the InlineParameterEditor.
this.forceUpdate();
if (this._searchPanel)
this._searchPanel.markSearchResultsDirty();
}}
resourceManagementProps={resourceManagementProps}
/>
<ContextMenu
ref={eventContextMenu =>
(this.eventContextMenu = eventContextMenu)
}
buildMenuTemplate={this._buildEventContextMenu}
/>
<ContextMenu
ref={instructionContextMenu =>
(this.instructionContextMenu = instructionContextMenu)
}
buildMenuTemplate={this._buildInstructionContextMenu}
/>
</div>
{isFunctionOnlyCallingItself && (
<Line>
<Column expand>
<AlertMessage kind="warning">
<Trans>
This function calls itself (it is "recursive"). Ensure
this is expected and there is a proper condition to stop
it if necessary.
</Trans>
</AlertMessage>
</Column>
</Line>
)}
</EventsSearcher>
{this._containerDivLastKnownSize && (
<EventsTree
ref={eventsTree => (this._eventsTree = eventsTree)}
key={events.ptr}
indentScale={preferences.values.eventsSheetIndentScale}
onScroll={this._ensureFocused}
events={events}
project={project}
scope={scope}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
projectScopedContainersAccessor={
projectScopedContainersAccessor
}
selection={this.state.selection}
onInstructionClick={this.selectInstruction}
onInstructionDoubleClick={this.openInstructionEditor}
onInstructionContextMenu={this.openInstructionContextMenu}
onAddInstructionContextMenu={
this.openAddInstructionContextMenu
}
onAddNewInstruction={this.openInstructionEditor}
onPasteInstructions={this.pasteInstructionsInInstructionsList}
onMoveToInstruction={this.moveSelectionToInstruction}
onMoveToInstructionsList={
this.moveSelectionToInstructionsList
}
onParameterClick={this.openParameterEditor}
onVariableDeclarationClick={() => {
// Nothing to do.
}}
onVariableDeclarationDoubleClick={this.openVariablesEditor}
onEventClick={this.selectEvent}
onEventContextMenu={this.openEventContextMenu}
onAddNewEvent={(
eventType: string,
eventsList: gdEventsList
) => {
this.addNewEvent(eventType, {
eventsList,
indexInList: eventsList.getEventsCount(),
});
}}
onOpenExternalEvents={onOpenExternalEvents}
onOpenLayout={onOpenLayout}
searchResults={eventsSearchResultEvents}
searchFocusOffset={searchFocusOffset}
onEventMoved={this._onEventMoved}
onEndEditingEvent={this._onEndEditingStringEvent}
showObjectThumbnails={
preferences.values.eventsSheetShowObjectThumbnails
}
screenType={screenType}
windowSize={windowSize}
eventsSheetWidth={this._containerDivLastKnownSize.width}
eventsSheetHeight={this._containerDivLastKnownSize.height}
fontSize={preferences.values.eventsSheetZoomLevel}
preferences={preferences}
tutorials={tutorials}
/>
)}
{this.state.showSearchPanel && (
<ErrorBoundary
componentTitle={<Trans>Search panel</Trans>}
scope="scene-events-search"
onClose={() => this._closeSearchPanel()}
>
<SearchPanel
ref={searchPanel => (this._searchPanel = searchPanel)}
onSearchInEvents={inputs =>
this._searchInEvents(searchInEvents, inputs)
}
onReplaceInEvents={inputs => {
this._replaceInEvents(replaceInEvents, inputs);
}}
resultsCount={
eventsSearchResultEvents
? eventsSearchResultEvents.length
: null
}
hasEventSelected={hasEventSelected(this.state.selection)}
onGoToPreviousSearchResult={() =>
this._ensureUnfoldedAndScrollTo(goToPreviousSearchResult)
}
onCloseSearchPanel={() => {
this._closeSearchPanel();
}}
onGoToNextSearchResult={() =>
this._ensureUnfoldedAndScrollTo(goToNextSearchResult)
}
searchFocusOffset={searchFocusOffset}
/>
</ErrorBoundary>
)}
<InlineParameterEditor
open={this.state.inlineEditing}
anchorEl={this.state.inlineEditingAnchorEl}
onRequestClose={() => {
this.closeParameterEditor(
/*shouldCancel=*/ preferences.values
.eventsSheetCancelInlineParameter === 'cancel'
);
}}
onApply={() => {
this.closeParameterEditor(/*shouldCancel=*/ false);
}}
project={project}
scope={scope}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
projectScopedContainersAccessor={
editedParameterProjectScopedContainersAccessor
}
isCondition={this.state.editedParameter.isCondition}
instruction={this.state.editedParameter.instruction}
parameterIndex={this.state.editedParameter.parameterIndex}
onChange={value => {
const {
instruction,
parameterIndex,
} = this.state.editedParameter;
if (!instruction || !this.state.inlineEditing) {
// Unlikely to ever happen, but maybe a component could
// fire the "onChange" while the inline editor was just
// dismissed.
return;
}
instruction.setParameter(parameterIndex, value);
gd.VariableInstructionSwitcher.switchBetweenUnifiedInstructionIfNeeded(
project.getCurrentPlatform(),
editedParameterProjectScopedContainersAccessor.get(),
instruction
);
// Ask the component to re-render, so that the new parameter
// set for the instruction in the state
// is taken into account for the InlineParameterEditor.
this.forceUpdate();
if (this._searchPanel)
this._searchPanel.markSearchResultsDirty();
}}
resourceManagementProps={resourceManagementProps}
/>
<ContextMenu
ref={eventContextMenu =>
(this.eventContextMenu = eventContextMenu)
}
buildMenuTemplate={this._buildEventContextMenu}
/>
<ContextMenu
ref={instructionContextMenu =>
(this.instructionContextMenu = instructionContextMenu)
}
buildMenuTemplate={this._buildInstructionContextMenu}
/>
</div>
)}
</ResponsiveWindowMeasurer>
</EventsSearcher>
{this._renderInstructionEditorDialog()}
{this.state.analyzedEventsContextResult && (
<EventsContextAnalyzerDialog
@@ -2211,6 +2224,7 @@ const EventsSheet = (props, ref) => {
const preferences = React.useContext(PreferencesContext);
const { tutorials } = React.useContext(TutorialContext);
const leaderboardsManager = React.useContext(LeaderboardContext);
const { windowSize } = useResponsiveWindowSize();
const shortcutMap = useShortcutMap();
return (
<EventsSheetComponentWithoutHandle
@@ -2220,6 +2234,7 @@ const EventsSheet = (props, ref) => {
tutorials={tutorials}
leaderboardsManager={leaderboardsManager}
shortcutMap={shortcutMap}
windowSize={windowSize}
{...props}
/>
);

View File

@@ -401,10 +401,11 @@ const CustomObjectPropertiesEditor = (props: Props) => {
</ColumnStackLayout>
</>
)}
{!getVariantName(
{(!getVariantName(
eventBasedObject,
customObjectConfiguration
) &&
) ||
customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration()) &&
(eventBasedObject &&
(!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() &&
!customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ? (

View File

@@ -1092,6 +1092,11 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
[projectScopedContainersAccessor]
);
const isEntirelyEmpty =
objectsContainer.getObjectsCount() === 0 &&
(!globalObjectsContainer ||
globalObjectsContainer.getObjectsCount() === 0);
const getTreeViewData = React.useCallback(
(i18n: I18nType): Array<TreeViewItem> => {
const treeViewItems = [
@@ -1155,8 +1160,10 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
sceneObjectsRootFolderId,
i18n._(labels.localScopeObjectsTitle),
{
primary: true,
showPrimaryLabel: isEntirelyEmpty,
icon: <Add />,
label: t`Add an object`,
label: t`Add object`,
click: () => {
onAddNewObject(selectedObjectFolderOrObjectsWithContext[0]);
},
@@ -1216,6 +1223,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
onAddNewObject,
selectedObjectFolderOrObjectsWithContext,
onExportAssets,
isEntirelyEmpty,
]
);

View File

@@ -24,6 +24,7 @@
align-items: center;
height: 100%;
position: relative;
min-width: 0;
--horizontal-padding: 21px;
}
@@ -38,8 +39,9 @@
}
.rowContentSide.rowContentSideRight {
/* Used for a better dragging preview. */
position: absolute;
right: 0;
/* position: absolute;
right: 0; */
flex-shrink: 0;
z-index: 1;
}

View File

@@ -0,0 +1,61 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import Button from '@material-ui/core/Button';
import { type ButtonInterface } from '../../UI/Button';
import { Spacer } from '../../UI/Grid';
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
export type TreeViewRightPrimaryButtonProps = {|
label?: MessageDescriptor,
disabled?: boolean,
icon?: React.Node,
id?: ?string,
onClick: ?(MouseEvent) => void | Promise<void>,
|};
export const TreeViewRightPrimaryButton = React.forwardRef<
TreeViewRightPrimaryButtonProps,
ButtonInterface
>(
(
{ label, icon, disabled, ...otherProps }: TreeViewRightPrimaryButtonProps,
ref
) => {
// In theory, focus ripple is only shown after a keyboard interaction
// (see https://github.com/mui-org/material-ui/issues/12067). However, as
// it's important to get focus right in the whole app, make the ripple
// always visible to be sure we're getting focusing right.
const focusRipple = true;
return (
<I18n>
{({ i18n }) => (
<Button
variant="contained"
size="small"
disableElevation
color="primary"
focusRipple={focusRipple}
disabled={disabled}
style={{
// If no label is specified, reduce the min width so that the button
// is just around the icon.
minWidth: !label ? 0 : undefined,
padding: label ? '2px 8px 2px 2px' : '2px 2px 2px 2px',
marginRight: label ? 2 : 0,
flexShrink: 0,
}}
{...otherProps}
ref={ref}
>
{icon}
{!!icon && !!label && <Spacer />}
{/* span element is required to prevent browser auto translators to crash the app - See https://github.com/4ian/GDevelop/issues/3453 */}
{label ? <span>{i18n._(label)}</span> : null}
</Button>
)}
</I18n>
);
}
);

View File

@@ -21,6 +21,7 @@ import { useLongTouch } from '../../Utils/UseLongTouch';
import { dataObjectToProps } from '../../Utils/HTMLDataset';
import { type DraggedItem } from '../DragAndDrop/DragSourceAndDropTarget';
import classNames from 'classnames';
import { TreeViewRightPrimaryButton } from './TreeViewRightPrimaryButton';
const stopPropagation = e => e.stopPropagation();
@@ -447,26 +448,47 @@ const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => {
)}
>
{node.rightComponent}
{rightButton && (
<IconButton
id={rightButton.id}
size="small"
onClick={e => {
e.stopPropagation();
if (rightButton.click) {
rightButton.click();
{rightButton &&
(rightButton.primary ? (
<TreeViewRightPrimaryButton
onClick={e => {
e.stopPropagation();
if (rightButton.click) {
rightButton.click();
}
}}
label={
rightButton.showPrimaryLabel
? rightButton.label
: null
}
}}
tooltip={rightButton.label}
disabled={
rightButton.enabled === undefined
? false
: !rightButton.enabled
}
>
{rightButton.icon}
</IconButton>
)}
disabled={
rightButton.enabled === undefined
? false
: !rightButton.enabled
}
icon={rightButton.icon}
/>
) : (
<IconButton
id={rightButton.id}
size="small"
onClick={e => {
e.stopPropagation();
if (rightButton.click) {
rightButton.click();
}
}}
tooltip={rightButton.label}
disabled={
rightButton.enabled === undefined
? false
: !rightButton.enabled
}
>
{rightButton.icon}
</IconButton>
))}
{shouldDisplayMenu && (
<IconButton
size="small"

View File

@@ -31,6 +31,8 @@ export type MenuButton = {|
label: MessageDescriptor,
click: ?() => void | Promise<void>,
enabled?: boolean,
primary?: boolean,
showPrimaryLabel?: boolean,
|};
type FlattenedNode<Item> = {|