Files
GDevelop/newIDE/app/src/EventsSheet/EventsTree/index.js
2025-01-06 17:58:18 +01:00

1112 lines
35 KiB
JavaScript

// @flow
import { Trans } from '@lingui/macro';
import React, { Component, type Node } from 'react';
import findIndex from 'lodash/findIndex';
import {
SortableTreeWithoutDndContext,
getFlatDataFromTree,
getNodeAtPath,
} from 'react-sortable-tree';
import { type ConnectDragSource } from 'react-dnd';
import { mapFor } from '../../Utils/MapFor';
import { getInitialSelection, isEventSelected } from '../SelectionHandler';
import EventsRenderingService from './EventsRenderingService';
import EventHeightsCache from './EventHeightsCache';
import classNames from 'classnames';
import {
eventsTree,
eventsTreeWithSearchResults,
handle,
icon,
} from './ClassNames';
import {
type SelectionState,
type EventContext,
type InstructionsListContext,
type InstructionContext,
type ParameterContext,
type VariableDeclarationContext,
} from '../SelectionHandler';
import { type EventsScope } from '../../InstructionOrExpression/EventsScope';
import getObjectByName from '../../Utils/GetObjectByName';
import ObjectsRenderingService from '../../ObjectsRendering/ObjectsRenderingService';
import { type ScreenType } from '../../UI/Responsive/ScreenTypeMeasurer';
import { type WindowSizeType } from '../../UI/Responsive/ResponsiveWindowMeasurer';
// Import default style of react-sortable-tree and the override made for EventsSheet.
import 'react-sortable-tree/style.css';
import './style.css';
import BottomButtons from './BottomButtons';
import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder';
import { CorsAwareImage } from '../../UI/CorsAwareImage';
import { Line } from '../../UI/Grid';
import { type Preferences } from '../../MainFrame/Preferences/PreferencesContext';
import { type Tutorial } from '../../Utils/GDevelopServices/Tutorial';
import TutorialMessage from '../../Hints/TutorialMessage';
import getTutorial from '../../Hints/getTutorial';
import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndDropTarget';
import { makeDropTarget } from '../../UI/DragAndDrop/DropTarget';
import { AutoScroll, DropContainer } from './DropContainer';
import { isDescendant, type MoveFunctionArguments } from './helpers';
import { dataObjectToProps } from '../../Utils/HTMLDataset';
import useForceUpdate from '../../Utils/UseForceUpdate';
import { useLongTouch } from '../../Utils/UseLongTouch';
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
const gd: libGDevelop = global.gd;
const eventsSheetEventsDnDType = 'events-sheet-events-dnd-type';
const getThumbnail = ObjectsRenderingService.getThumbnail.bind(
ObjectsRenderingService
);
const defaultIndentWidth = 22;
const smallIndentWidth = 11;
const styles = {
container: { flex: 1, position: 'relative' },
defaultEventContainer: {
marginRight: 10,
position: 'relative',
},
smallEventContainer: {
marginRight: 0,
position: 'relative',
},
eventComponentContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'stretch',
position: 'relative',
},
};
export const getIndentWidth = (windowSize: WindowSizeType) =>
windowSize === 'small' ? smallIndentWidth : defaultIndentWidth;
const getEventContainerStyle = (windowSize: WindowSizeType) =>
windowSize === 'small'
? styles.smallEventContainer
: styles.defaultEventContainer;
type EventsContainerProps = {|
eventsHeightsCache: EventHeightsCache,
event: gdBaseEvent,
leftIndentWidth: number,
disabled: boolean,
project: gdProject,
scope: EventsScope,
globalObjectsContainer: gdObjectsContainer,
objectsContainer: gdObjectsContainer,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
selection: SelectionState,
onAddNewInstruction: InstructionsListContext => void,
onPasteInstructions: InstructionsListContext => void,
onMoveToInstruction: (destinationContext: InstructionContext) => void,
onMoveToInstructionsList: (
destinationContext: InstructionsListContext
) => void,
onInstructionClick: InstructionContext => void,
onInstructionDoubleClick: InstructionContext => void,
onInstructionContextMenu: (x: number, y: number, InstructionContext) => void,
onAddInstructionContextMenu: (
HTMLButtonElement,
InstructionsListContext
) => void,
onParameterClick: ParameterContext => void,
onVariableDeclarationClick: VariableDeclarationContext => void,
onVariableDeclarationDoubleClick: VariableDeclarationContext => void,
onEventClick: (eventContext: EventContext) => void,
onEndEditingEvent: () => void,
onEventContextMenu: (x: number, y: number) => void,
onOpenExternalEvents: string => void,
onOpenLayout: string => void,
renderObjectThumbnail: string => Node,
screenType: ScreenType,
eventsSheetWidth: number,
eventsSheetHeight: number,
connectDragSource: ConnectDragSource,
windowSize: WindowSizeType,
idPrefix: string,
|};
const hiddenBecauseHeightNotComputedYetStyle = {
visibility: 'hidden',
};
/**
* The component containing an event.
* It will report the rendered event height so that the EventsTree can
* update accordingly.
*/
const EventContainer = (props: EventsContainerProps) => {
const {
event,
project,
scope,
disabled,
eventsHeightsCache,
onEventContextMenu,
projectScopedContainersAccessor,
} = props;
const forceUpdate = useForceUpdate();
const containerRef = React.useRef<?HTMLDivElement>(null);
const height = containerRef.current ? containerRef.current.offsetHeight : 0;
React.useEffect(
() => {
eventsHeightsCache.setEventHeight(event, height);
},
[event, eventsHeightsCache, height]
);
const _onEventContextMenu = React.useCallback(
(domEvent: MouseEvent) => {
domEvent.preventDefault();
onEventContextMenu(domEvent.clientX, domEvent.clientY);
},
[onEventContextMenu]
);
const longTouchForContextMenuProps = useLongTouch(
React.useCallback(
(domEvent: any) => {
onEventContextMenu(domEvent.clientX, domEvent.clientY);
},
[onEventContextMenu]
),
{ context: 'events-tree-event-component' }
);
const EventComponent = EventsRenderingService.getEventComponent(event);
return (
<div
ref={containerRef}
onClick={props.onEventClick}
onContextMenu={_onEventContextMenu}
{...longTouchForContextMenuProps}
style={
eventsHeightsCache.getEventHeight(event)
? undefined
: hiddenBecauseHeightNotComputedYetStyle
}
>
{!!EventComponent && (
<div style={styles.eventComponentContainer}>
{props.connectDragSource(<div className={handle} />)}
<div style={styles.container}>
<EventComponent
project={project}
scope={scope}
event={event}
globalObjectsContainer={props.globalObjectsContainer}
objectsContainer={props.objectsContainer}
projectScopedContainersAccessor={projectScopedContainersAccessor}
selected={isEventSelected(props.selection, event)}
selection={props.selection}
leftIndentWidth={props.leftIndentWidth}
onUpdate={forceUpdate}
onAddNewInstruction={props.onAddNewInstruction}
onPasteInstructions={props.onPasteInstructions}
onMoveToInstruction={props.onMoveToInstruction}
onMoveToInstructionsList={props.onMoveToInstructionsList}
onInstructionClick={props.onInstructionClick}
onInstructionDoubleClick={props.onInstructionDoubleClick}
onInstructionContextMenu={props.onInstructionContextMenu}
onAddInstructionContextMenu={props.onAddInstructionContextMenu}
onVariableDeclarationClick={props.onVariableDeclarationClick}
onVariableDeclarationDoubleClick={
props.onVariableDeclarationDoubleClick
}
onEndEditingEvent={props.onEndEditingEvent}
onParameterClick={props.onParameterClick}
onOpenExternalEvents={props.onOpenExternalEvents}
onOpenLayout={props.onOpenLayout}
disabled={
disabled /* Use disabled (not event.disabled) as it is true if a parent event is disabled*/
}
renderObjectThumbnail={props.renderObjectThumbnail}
screenType={props.screenType}
eventsSheetWidth={props.eventsSheetWidth}
eventsSheetHeight={props.eventsSheetHeight}
windowSize={props.windowSize}
idPrefix={props.idPrefix}
/>
</div>
</div>
)}
</div>
);
};
const SortableTree = ({ className, ...otherProps }) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<SortableTreeWithoutDndContext
className={`${eventsTree} ${
gdevelopTheme.palette.type === 'light' ? 'light-theme' : 'dark-theme'
} ${className}`}
{...otherProps}
/>
);
};
const noop = () => {};
type EventsTreeProps = {|
events: gdEventsList,
project: gdProject,
scope: EventsScope,
globalObjectsContainer: gdObjectsContainer,
objectsContainer: gdObjectsContainer,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
selection: SelectionState,
onAddNewInstruction: (
eventContext: EventContext,
InstructionsListContext
) => void,
onPasteInstructions: (
eventContext: EventContext,
InstructionsListContext
) => void,
onMoveToInstruction: (
eventContext: EventContext,
destinationContext: InstructionContext
) => void,
onMoveToInstructionsList: (
eventContext: EventContext,
destinationContext: InstructionsListContext
) => void,
onInstructionClick: (
eventContext: EventContext,
instructionContext: InstructionContext
) => void,
onInstructionDoubleClick: (
eventContext: EventContext,
instructionContext: InstructionContext
) => void,
onInstructionContextMenu: (
eventContext: EventContext,
x: number,
y: number,
InstructionContext
) => void,
onAddInstructionContextMenu: (
eventContext: EventContext,
HTMLButtonElement,
InstructionsListContext
) => void,
onParameterClick: (
eventContext: EventContext,
parameterContext: ParameterContext
) => void,
onVariableDeclarationClick: (
eventContext: EventContext,
variableDeclarationContext: VariableDeclarationContext
) => void,
onVariableDeclarationDoubleClick: (
eventContext: EventContext,
variableDeclarationContext: VariableDeclarationContext
) => void,
onEventClick: (eventContext: EventContext) => void,
onEventContextMenu: (
x: number,
y: number,
eventContext: EventContext
) => void,
onAddNewEvent: (eventType: string, eventsList: gdEventsList) => void,
onOpenExternalEvents: string => void,
onOpenLayout: string => void,
showObjectThumbnails: boolean,
searchResults: ?Array<gdBaseEvent>,
searchFocusOffset: ?number,
onEventMoved: (previousRowIndex: number, nextRowIndex: number) => void,
onEndEditingEvent: (event: gdBaseEvent) => void,
onScroll?: () => void,
initialScrollPosition?: number,
screenType: ScreenType,
windowSize: WindowSizeType,
eventsSheetWidth: number,
eventsSheetHeight: number,
fontSize?: number,
indentScale: number,
preferences: Preferences,
tutorials: ?Array<Tutorial>,
|};
// A node displayed by the SortableTree. Almost always represents an
// event, except for the buttons at the bottom of the sheet and the tutorial.
export type SortableTreeNode = {|
// Necessary attributes for react-sortable-tree.
title: (node: {| node: SortableTreeNode |}) => Node,
children: Array<any>,
expanded: boolean,
eventsList: gdEventsList,
event: ?gdBaseEvent,
depth: number,
disabled: boolean,
indexInList: number,
rowIndex: number,
nodePath: Array<number>,
relativeNodePath: Array<number>,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
// Key is event pointer or an identification string.
key: number | string,
// In case of nodes without event (buttons at the bottom of the sheet),
// use a fixed height.
fixedHeight?: ?number,
|};
type State = {|
treeData: Array<any>,
flatData: Array<gdBaseEvent>,
draggedNode: ?SortableTreeNode,
isScrolledTop: boolean,
isScrolledBottom: boolean,
|};
const getNodeKey = ({ treeIndex }) => treeIndex;
/**
* Display a tree of event. Builtin on react-sortable-tree so that event
* can be drag'n'dropped and events rows are virtualized.
*/
export default class ThemableEventsTree extends Component<
EventsTreeProps,
State
> {
static defaultProps = {
selection: getInitialSelection(),
};
_list: ?any;
eventsHeightsCache: EventHeightsCache;
DragSourceAndDropTarget = makeDragSourceAndDropTarget<SortableTreeNode>(
eventsSheetEventsDnDType
);
DropTarget = makeDropTarget<SortableTreeNode>(eventsSheetEventsDnDType);
temporaryUnfoldedNodes: Array<SortableTreeNode>;
_hoverTimerId: ?TimeoutID;
_isForcedToInitialScroll: boolean;
constructor(props: EventsTreeProps) {
super(props);
this.temporaryUnfoldedNodes = [];
this.eventsHeightsCache = new EventHeightsCache(this);
this.state = {
...this._eventsToTreeData(
props.projectScopedContainersAccessor,
props.events
),
draggedNode: null,
isScrolledTop: true,
isScrolledBottom: false,
};
this._isForcedToInitialScroll = !!this.props.initialScrollPosition;
}
componentDidMount() {
this.onHeightsChanged();
}
componentDidUpdate(prevProps: EventsTreeProps) {
const {
values: { hiddenTutorialHints },
} = this.props.preferences;
const {
values: { hiddenTutorialHints: previousHiddenTutorialHints },
} = prevProps.preferences;
if (
hiddenTutorialHints['intro-event-system'] !==
previousHiddenTutorialHints['intro-event-system']
) {
this.setState({
...this.state,
treeData: this.state.treeData.filter(
data => data.key !== 'eventstree-tutorial-node'
),
});
}
}
componentWillUnmount() {
this._hoverTimerId && clearTimeout(this._hoverTimerId);
}
/**
* Should be called whenever an event height has changed
*/
onHeightsChanged(cb: ?() => void) {
this.forceUpdate(() => {
const currentList = this._list;
if (currentList) {
const listWrapper = currentList.wrappedInstance.current;
if (listWrapper) {
listWrapper.recomputeRowHeights();
}
}
if (cb) cb();
});
}
/**
* Should be called whenever events changed (new event...)
* from outside this component.
*/
forceEventsUpdate(cb: ?() => void) {
this.setState(
this._eventsToTreeData(
this.props.projectScopedContainersAccessor,
this.props.events
),
() => {
if (this._list && this._list.wrappedInstance.current) {
this._list.wrappedInstance.current.recomputeRowHeights();
}
if (cb) cb();
}
);
}
scrollToRow(row: number) {
if (row !== -1) {
const currentList = this._list;
if (currentList) {
const listWrapper = currentList.wrappedInstance.current;
listWrapper && listWrapper.scrollToRow(row);
}
}
}
getScrollPosition(): number {
const currentList = this._list;
if (!currentList) {
return 0;
}
const listWrapper = currentList.wrappedInstance.current;
return listWrapper ? listWrapper.Grid.state.scrollTop : 0;
}
/**
* Unfold events so that the given one is visible
*/
unfoldForEvent(event: gdBaseEvent) {
gd.EventsListUnfolder.unfoldWhenContaining(this.props.events, event);
this.forceEventsUpdate();
}
foldAll() {
gd.EventsListUnfolder.foldAll(this.props.events);
this.forceEventsUpdate();
}
unfoldToLevel(level: number) {
gd.EventsListUnfolder.unfoldToLevel(this.props.events, level);
this.forceEventsUpdate();
}
getEventRow(searchedEvent: gdBaseEvent) {
// TODO: flatData could be replaced by a hashmap of events to row index
return findIndex(
this.state.flatData,
event => event.ptr === searchedEvent.ptr
);
}
getEventContextAtRowIndexes(rowIndexes: Array<number>): Array<EventContext> {
// We use flatDataTree instead of this.state.flatData because we need the events contexts too.
const flatDataTree: Array<{ node: SortableTreeNode }> = getFlatDataFromTree(
{
treeData: this.state.treeData,
getNodeKey,
ignoreCollapsed: true,
}
);
return rowIndexes
.map(rowIndex => {
if (!flatDataTree[rowIndex]) return null;
const {
node: {
event,
eventsList,
indexInList,
projectScopedContainersAccessor,
},
} = flatDataTree[rowIndex];
return event
? { event, eventsList, indexInList, projectScopedContainersAccessor }
: null;
})
.filter(Boolean);
}
_eventsToTreeData = (
parentProjectScopedContainersAccessor: ProjectScopedContainersAccessor,
eventsList: gdEventsList,
flatData: Array<gdBaseEvent> = [],
depth: number = 0,
parentDisabled: boolean = false,
parentAbsolutePath: Array<number> = [],
parentRelativePath: ?Array<number> = null
) => {
const treeData = mapFor<SortableTreeNode>(
0,
eventsList.getEventsCount(),
i => {
const event = eventsList.getEventAt(i);
flatData.push(event);
const disabled = parentDisabled || event.isDisabled();
const absoluteIndex = flatData.length - 1;
const currentAbsolutePath = parentAbsolutePath.concat(
flatData.length - 1
);
const currentRelativePath = [...(parentRelativePath || []), i];
const projectScopedContainersAccessor = event.canHaveVariables()
? parentProjectScopedContainersAccessor.makeNewProjectScopedContainersWithLocalVariables(
event
)
: parentProjectScopedContainersAccessor;
return {
title: this._renderEvent,
event,
eventsList,
indexInList: i,
rowIndex: absoluteIndex,
expanded: !event.isFolded(),
disabled,
depth,
key: event.ptr, //TODO: useless?
children: this._eventsToTreeData(
projectScopedContainersAccessor,
event.getSubEvents(),
// flatData is a flat representation of events, one for each line.
// Hence it should not contain the folded events.
!event.isFolded() ? flatData : [],
depth + 1,
disabled,
currentAbsolutePath,
currentRelativePath
).treeData,
nodePath: currentAbsolutePath,
relativeNodePath: currentRelativePath,
projectScopedContainersAccessor,
};
}
);
const tutorial = getTutorial(
this.props.preferences,
this.props.tutorials,
'intro-event-system'
);
// Add the bottom buttons if we're at the root
const extraNodes = [
depth === 0
? {
title: () => (
<BottomButtons
onAddEvent={(eventType: string) =>
this.props.onAddNewEvent(eventType, this.props.events)
}
DnDComponent={this.DropTarget}
draggedNode={this.state.draggedNode}
rootEventsList={eventsList}
/>
),
event: null,
indexInList: eventsList.getEventsCount(),
disabled: false,
depth: 0,
fixedHeight: 40,
children: [],
}
: null,
depth === 0 && eventsList.getEventsCount() !== 0 && tutorial
? {
title: () => (
<Line justifyContent="center">
<TutorialMessage tutorial={tutorial} />
</Line>
),
event: null,
indexInList: eventsList.getEventsCount() + 1,
disabled: false,
depth: 0,
fixedHeight: 150,
children: [],
key: 'eventstree-tutorial-node',
}
: null,
depth === 0 && eventsList.getEventsCount() === 0
? {
title: () => (
<EmptyPlaceholder
title={<Trans>Add your first event</Trans>}
description={<Trans>Events define the rules of a game.</Trans>}
actionLabel={<Trans>Add an event</Trans>}
helpPagePath="/events"
tutorialId="intro-event-system"
actionButtonId="add-event-button"
onAction={() =>
this.props.onAddNewEvent(
'BuiltinCommonInstructions::Standard',
this.props.events
)
}
/>
),
event: null,
indexInList: eventsList.getEventsCount() + 1,
disabled: false,
depth: 0,
fixedHeight: 300,
children: [],
}
: null,
].filter(Boolean);
return {
// $FlowFixMe - We are confident treeData and extraNodes are both arrays of SortableTreeNode
treeData: extraNodes.length ? treeData.concat(extraNodes) : treeData,
flatData,
};
};
_canDrag = (node: ?SortableTreeNode) => {
return !!node && !!node.event;
};
_canDrop = (hoveredNode: SortableTreeNode) => {
return true;
};
_onDrop = (
moveFunction: MoveFunctionArguments => void,
currentNode: SortableTreeNode
) => {
const draggedNode = this.state.draggedNode;
if (draggedNode) {
moveFunction({
node: draggedNode,
targetNode: currentNode,
});
const { nodePath, event } = draggedNode;
this._onEndDrag();
if (!event) {
console.warn('EventsSheet: No event found in dragged node.');
return;
}
const newRowIndex = this.getEventRow(event);
this.props.onEventMoved(nodePath[nodePath.length - 1], newRowIndex);
}
};
_onVisibilityToggle = ({ node }: {| node: SortableTreeNode |}) => {
const { event } = node;
if (!event) return;
event.setFolded(!event.isFolded());
this.forceEventsUpdate();
};
_renderObjectThumbnail = (objectName: string) => {
const {
project,
globalObjectsContainer,
objectsContainer,
showObjectThumbnails,
} = this.props;
if (!showObjectThumbnails) return null;
const object = getObjectByName(
globalObjectsContainer,
objectsContainer,
objectName
);
if (!object) return null;
return (
<CorsAwareImage
className={classNames({
[icon]: true,
})}
alt=""
src={getThumbnail(project, object.getConfiguration())}
/>
);
};
_temporaryUnfoldNode = (isOverLazy: boolean, node: SortableTreeNode) => {
const { event } = node;
if (!event) return;
const isNodeTemporaryUnfolded = this.temporaryUnfoldedNodes.some(
foldedNode => node.key === foldedNode.key
);
if (isOverLazy) {
if (!this._hoverTimerId && !node.expanded) {
if (!isNodeTemporaryUnfolded) {
this._hoverTimerId = window.setTimeout(() => {
// $FlowFixMe - Per the condition above, we are confident that node.event is not null.
event.setFolded(false);
this.temporaryUnfoldedNodes.push(node);
this.forceEventsUpdate();
}, 1000);
}
}
} else {
window.clearTimeout(this._hoverTimerId);
this._hoverTimerId = null;
}
};
_restoreFoldedNodes = () => {
this.temporaryUnfoldedNodes.forEach(
node => node.event && node.event.setFolded(true)
);
this.temporaryUnfoldedNodes = [];
this.forceEventsUpdate();
};
_getRowHeight = ({ node }: {| node: ?SortableTreeNode |}) => {
if (!node) return 0;
if (!node.event) return node.fixedHeight || 0;
return this.eventsHeightsCache.getEventHeight(node.event);
};
_onEndDrag = () => {
// This method is always called at the end of the drag, regardless of whether
// an event was actually dropped. It is also already called in `_onDrop` to update
// the event list and compute history. So if draggedNode is null, we want to avoid
// recomputing the event list.
if (this.state.draggedNode) {
this.setState({ draggedNode: null });
this._restoreFoldedNodes();
this.forceEventsUpdate();
}
};
_renderEvent = ({ node }: {| node: SortableTreeNode |}) => {
const { event, depth, disabled } = node;
if (!event) return null;
const { DragSourceAndDropTarget, DropTarget } = this;
const isDragged =
!!this.state.draggedNode &&
(isDescendant(this.state.draggedNode, node) ||
node.key === this.state.draggedNode.key);
return (
<DragSourceAndDropTarget
beginDrag={() => {
this.setState({ draggedNode: node });
return node;
}}
canDrag={() => this._canDrag(node)}
canDrop={() => this._canDrop(node)}
// Drop operations are handled by DropContainers.
drop={() => {
return;
}}
endDrag={this._onEndDrag}
>
{({ connectDragSource, connectDropTarget, isOverLazy }) => {
this._temporaryUnfoldNode(isOverLazy, node);
const eventContext = {
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
projectScopedContainersAccessor:
node.projectScopedContainersAccessor,
};
const dropTarget = (
<div
style={{
opacity: isDragged ? 0.5 : 1,
...getEventContainerStyle(this.props.windowSize),
}}
{...dataObjectToProps({ rowIndex: node.rowIndex.toString() })}
>
<EventContainer
project={this.props.project}
scope={this.props.scope}
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
projectScopedContainersAccessor={
node.projectScopedContainersAccessor
}
event={event}
key={event.ptr}
eventsHeightsCache={this.eventsHeightsCache}
selection={this.props.selection}
leftIndentWidth={
depth *
(getIndentWidth(this.props.windowSize) *
this.props.indentScale)
}
onAddNewInstruction={instructionsListContext =>
this.props.onAddNewInstruction(
eventContext,
instructionsListContext
)
}
onPasteInstructions={instructionsListContext =>
this.props.onPasteInstructions(
eventContext,
instructionsListContext
)
}
onMoveToInstruction={instructionContext =>
this.props.onMoveToInstruction(
eventContext,
instructionContext
)
}
onMoveToInstructionsList={instructionContext =>
this.props.onMoveToInstructionsList(
eventContext,
instructionContext
)
}
onInstructionClick={instructionContext =>
this.props.onInstructionClick(
eventContext,
instructionContext
)
}
onInstructionDoubleClick={instructionContext =>
this.props.onInstructionDoubleClick(
eventContext,
instructionContext
)
}
onParameterClick={parameterContext =>
this.props.onParameterClick(eventContext, parameterContext)
}
onVariableDeclarationClick={variableDeclarationContext =>
this.props.onVariableDeclarationClick(
eventContext,
variableDeclarationContext
)
}
onVariableDeclarationDoubleClick={variableDeclarationContext =>
this.props.onVariableDeclarationDoubleClick(
eventContext,
variableDeclarationContext
)
}
onEventClick={() =>
this.props.onEventClick({
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
projectScopedContainersAccessor:
node.projectScopedContainersAccessor,
})
}
onEndEditingEvent={() => this.props.onEndEditingEvent(event)}
onEventContextMenu={(x, y) =>
this.props.onEventContextMenu(x, y, {
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
projectScopedContainersAccessor:
node.projectScopedContainersAccessor,
})
}
onInstructionContextMenu={(...args) =>
this.props.onInstructionContextMenu(eventContext, ...args)
}
onAddInstructionContextMenu={(...args) =>
this.props.onAddInstructionContextMenu(eventContext, ...args)
}
onOpenExternalEvents={this.props.onOpenExternalEvents}
onOpenLayout={(name: string) => {
this.props.onOpenLayout(name);
}}
disabled={
disabled /* Use node.disabled (not event.disabled) as it is true if a parent event is disabled*/
}
renderObjectThumbnail={this._renderObjectThumbnail}
screenType={this.props.screenType}
eventsSheetWidth={this.props.eventsSheetWidth}
eventsSheetHeight={this.props.eventsSheetHeight}
connectDragSource={connectDragSource}
windowSize={this.props.windowSize}
idPrefix={`event-${node.relativeNodePath.join('-')}`}
/>
{this.state.draggedNode && (
<DropContainer
node={node}
draggedNode={this.state.draggedNode}
draggedNodeHeight={this._getRowHeight({
node: this.state.draggedNode,
})}
DnDComponent={DropTarget}
onDrop={this._onDrop}
activateTargets={!isDragged && !!this.state.draggedNode}
windowSize={this.props.windowSize}
indentScale={this.props.indentScale}
getNodeAtPath={path =>
getNodeAtPath({
path,
treeData: this.state.treeData,
getNodeKey,
}).node
}
/>
)}
</div>
);
return isDragged ? dropTarget : connectDropTarget(dropTarget);
}}
</DragSourceAndDropTarget>
);
};
_isNodeHighlighted = ({
node,
searchQuery,
}: {
node: SortableTreeNode,
searchQuery: ?Array<gdBaseEvent>,
}) => {
const searchResults = searchQuery;
if (!searchResults) return false;
const { event } = node;
if (!event) return false;
return searchResults.find(highlightedEvent =>
gd.compare(highlightedEvent, event)
);
};
_scrollUp = () => {
this._list && this._list.container.scrollBy({ top: -5 });
};
_scrollDown = () => {
this._list && this._list.container.scrollBy({ top: 5 });
};
render() {
// react-sortable-tree does the rendering by transforming treeData
// into a flat array, the result being memoized. This hack forces
// a re-rendering of events, by discarding the memoized flat array
// (otherwise, no re-rendering would be done).
const treeData = this.state.treeData ? [...this.state.treeData] : null;
const zoomLevel = this.props.fontSize || 14;
if (this._isForcedToInitialScroll) {
if (this.props.searchResults) {
this._isForcedToInitialScroll = false;
}
else {
const currentList = this._list;
if (currentList) {
const listWrapper = currentList.wrappedInstance.current;
if (listWrapper && listWrapper.Grid.state.scrollTop !== this.props.initialScrollPosition) {
// For some reason the List scroll is reset to 0 twice when the component is mounted.
listWrapper.scrollToPosition(this.props.initialScrollPosition);
}
}
}
}
return (
<div
style={{
...styles.container,
fontSize: `${zoomLevel}px`,
'--icon-size': `${Math.round(zoomLevel * 1.14)}px`,
'--instruction-missing-parameter-min-height': `${Math.round(
zoomLevel * 1.1
)}px`,
'--instruction-missing-parameter-min-width': `${Math.round(
zoomLevel * 3
)}px`,
}}
>
{/* Disable for touchscreen because the dragged DOM node gets deleted, the */}
{/* touch events are lost and the dnd does not drop anymore (hypothesis). */}
{this.props.screenType !== 'touch' && (
<>
<AutoScroll
DnDComponent={this.DropTarget}
direction="top"
activateTargets={
!!this.state.draggedNode && !this.state.isScrolledTop
}
onHover={this._scrollUp}
/>
<AutoScroll
DnDComponent={this.DropTarget}
direction="bottom"
activateTargets={
!!this.state.draggedNode && !this.state.isScrolledBottom
}
onHover={this._scrollDown}
/>
</>
)}
<SortableTree
treeData={treeData}
scaffoldBlockPxWidth={
getIndentWidth(this.props.windowSize) * this.props.indentScale
}
onChange={noop}
onVisibilityToggle={this._onVisibilityToggle}
canDrag={false}
rowHeight={this._getRowHeight}
searchMethod={this._isNodeHighlighted}
searchQuery={this.props.searchResults}
searchFocusOffset={this.props.searchFocusOffset}
className={
this.props.searchResults ? eventsTreeWithSearchResults : ''
}
reactVirtualizedListProps={{
ref: list => (this._list = list),
onScroll: event => {
if (
event.scrollTop !== 0 &&
event.scrollTop !== this.props.initialScrollPosition
) {
this._isForcedToInitialScroll = false;
}
this.props.onScroll && this.props.onScroll();
this.setState({
isScrolledTop: event.scrollTop === 0,
isScrolledBottom:
event.clientHeight + event.scrollTop >= event.scrollHeight,
});
},
scrollToAlignment: 'center',
}}
// Disable slideRegionSize on touchscreen because of a bug that makes scrolling
// uncontrollable on touchscreens. Ternary operator does not update slideRegionSize
// well.
slideRegionSize={-10}
/>
</div>
);
}
}