Rework events Drag&Drop in events sheet (#3936)

This commit is contained in:
Fannie Yan
2022-05-27 10:41:43 +02:00
committed by GitHub
parent 20075411bf
commit 2c5374b87a
18 changed files with 1030 additions and 285 deletions

View File

@@ -160,6 +160,23 @@
"frame-border-color": {
"value": "#000000"
}
},
"drop-indicator": {
"can-drop": {
"color": {
"value": "{gdevelop.color.dark-blue.value}"
}
},
"cannot-drop": {
"color": {
"value": "#ff5c5c"
}
},
"border": {
"color": {
"value": "{theme.surface.canvas.background-color.value}"
}
}
}
},
"input": {

View File

@@ -4,15 +4,28 @@ import * as React from 'react';
import { Line, Column } from '../../UI/Grid';
import ElementWithMenu from '../../UI/Menu/ElementWithMenu';
import { enumerateEventsMetadata } from '../EnumerateEventsMetadata';
import { type DropTargetComponent } from '../../UI/DragAndDrop/DropTarget';
import { type SortableTreeNode } from '.';
import { moveEventToEventsList } from './helpers';
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
const styles = {
addButton: {
cursor: 'pointer',
},
dropIndicator: {
border: '4px solid black',
outline: '1px solid white',
},
};
type Props = {|
onAddEvent: (eventType: string) => void,
// Connect a drop target to be able to drop an event at the end of the sheet.
DnDComponent: DropTargetComponent<SortableTreeNode>,
draggedNode: ?SortableTreeNode,
rootEventsList: gdEventsList,
|};
const makeMenuTemplateBuilderForEvents = (
@@ -25,33 +38,72 @@ const makeMenuTemplateBuilderForEvents = (
};
});
export default function BottomButtons({ onAddEvent }: Props) {
export default function BottomButtons({
onAddEvent,
DnDComponent,
draggedNode,
rootEventsList,
}: Props) {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const onDrop = () => {
draggedNode &&
moveEventToEventsList({
targetEventsList: rootEventsList,
movingEvent: draggedNode.event,
initialEventsList: draggedNode.eventsList,
// Drops node at the end of root events list.
toIndex: -1,
});
};
return (
<Column>
<Line justifyContent="space-between">
<ElementWithMenu
openMenuWithSecondaryClick
element={
<button
style={styles.addButton}
className="add-link"
onClick={() => onAddEvent('BuiltinCommonInstructions::Standard')}
id="add-event-button"
>
<Trans>Add a new event</Trans>
</button>
}
buildMenuTemplate={makeMenuTemplateBuilderForEvents(onAddEvent)}
/>
<ElementWithMenu
element={
<button style={styles.addButton} className="add-link">
<Trans>Add...</Trans>
</button>
}
buildMenuTemplate={makeMenuTemplateBuilderForEvents(onAddEvent)}
/>
</Line>
</Column>
<DnDComponent canDrop={() => true} drop={onDrop}>
{({ connectDropTarget, isOver }) =>
connectDropTarget(
<div>
{isOver && (
<div
style={{
...styles.dropIndicator,
borderColor: gdevelopTheme.dropIndicator.canDrop,
outlineColor: gdevelopTheme.dropIndicator.border,
}}
/>
)}
<Column>
<Line justifyContent="space-between">
<ElementWithMenu
openMenuWithSecondaryClick
element={
<button
style={styles.addButton}
className="add-link"
onClick={() =>
onAddEvent('BuiltinCommonInstructions::Standard')
}
id="add-event-button"
>
<Trans>Add a new event</Trans>
</button>
}
buildMenuTemplate={makeMenuTemplateBuilderForEvents(
onAddEvent
)}
/>
<ElementWithMenu
element={
<button style={styles.addButton} className="add-link">
<Trans>Add...</Trans>
</button>
}
buildMenuTemplate={makeMenuTemplateBuilderForEvents(
onAddEvent
)}
/>
</Line>
</Column>
</div>
)
}
</DnDComponent>
);
}

View File

@@ -24,6 +24,7 @@ export const eventsTreeWithSearchResults = 'with-search-results';
export const dropIndicator = 'drop-indicator';
export const cantDropIndicator = 'cant-drop-indicator';
export const handle = 'move-handle';
export const linkContainer = 'link-container';

View File

@@ -0,0 +1,327 @@
// @flow
import * as React from 'react';
import { getIndentWidth, type SortableTreeNode } from '.';
import {
moveNodeAbove,
moveNodeBelow,
moveNodeAsSubEvent,
isSameDepthAndJustBelow,
} from './helpers';
import { type WidthType } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import './style.css';
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
import { type DropTargetComponent } from '../../UI/DragAndDrop/DropTarget';
const sharedStyles = {
dropArea: { zIndex: 1, position: 'absolute' },
dropIndicator: {
position: 'absolute',
zIndex: 2,
border: '4px solid black',
outline: '1px solid white',
},
autoScroll: {
width: '100%',
position: 'absolute',
height: '10%',
zIndex: 2,
},
};
type DropTargetContainerStyle = {|
left?: string,
right?: string,
top?: string,
bottom?: string,
width?: number,
height?: number,
|};
type TargetPositionStyles = { [position: string]: DropTargetContainerStyle };
const getTargetPositionStyles = (
indentWidth: number,
draggedNodeHeight: number,
isDraggedNodeChild: boolean
): TargetPositionStyles => ({
'bottom-left': { left: '0px', bottom: '0px', top: '50%', width: indentWidth },
'bottom-right': {
left: `${indentWidth}px`,
right: '0px',
bottom: '0px',
top: '50%',
},
top: { left: '0px', right: '0px', top: '0px', bottom: '50%' },
bottom: { left: '0px', right: '0px', top: '50%', bottom: '0px' },
'below-left': {
left: isDraggedNodeChild ? '0px' : '10px',
top: '100%',
height: draggedNodeHeight,
width: indentWidth,
},
'below-right': {
left: isDraggedNodeChild ? `${indentWidth + 10}px` : `${indentWidth}px`,
right: '0px',
top: '100%',
height: draggedNodeHeight,
},
});
const getIndicatorPositionStyles = (
indentWidth: number
): TargetPositionStyles => ({
bottom: { left: '0px', right: '0px', bottom: '-4px' },
'bottom-right': { left: `${indentWidth}px`, right: '0px', bottom: '-4px' },
top: { left: '0px', right: '0px', top: '-4px' },
});
function DropTargetContainer({
DnDComponent,
canDrop,
onDrop,
style,
}: {|
DnDComponent: any,
canDrop: () => boolean,
onDrop: () => void,
style: {
dropIndicator: DropTargetContainerStyle,
dropArea: DropTargetContainerStyle,
},
|}) {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<DnDComponent canDrop={canDrop} drop={onDrop}>
{({ isOver, connectDropTarget, canDrop }) => {
return connectDropTarget(
<div>
{/* Drop area */}
<div
style={{
...sharedStyles.dropArea,
...style.dropArea,
// Uncomment for debugging purposes.
// backgroundColor: 'lightblue',
// opacity: isOver ? 1 : 0,
}}
/>
{/* Drop indicator */}
{canDrop && isOver && (
<div
style={{
...sharedStyles.dropIndicator,
...style.dropIndicator,
borderColor: gdevelopTheme.dropIndicator.canDrop,
outlineColor: gdevelopTheme.dropIndicator.border,
}}
/>
)}
</div>
);
}}
</DnDComponent>
);
}
type DropContainerProps = {|
node: SortableTreeNode,
draggedNode: SortableTreeNode,
DnDComponent: DropTargetComponent<SortableTreeNode>,
onDrop: (
moveFunction: ({
targetNode: SortableTreeNode,
node: SortableTreeNode,
}) => void,
node: SortableTreeNode
) => void,
activateTargets: boolean,
// Computes drop areas and drop indicator indent.
windowWidth: WidthType,
// Used only for the node just above dragged node if it is an only child,
// so that drop area covers the whole dragged node's row in height.
draggedNodeHeight: number,
|};
/**
* DropContainer is composed of sub-containers of drop targets that allows us to identify
* where the mouse or touch is and drop the dragged node accordingly. At most, there will be 5
* drop targets: 1 at the top of the row (drop above), 2 at the bottom (drop below or as subevent
* while hovering target node), 2 below (drop below or as sub-event moving left/right).
*/
export function DropContainer({
node,
draggedNode,
DnDComponent,
onDrop,
activateTargets,
windowWidth,
draggedNodeHeight,
}: DropContainerProps) {
const isDraggedNodesOnlyChild =
node.children.length === 1 && node.children[0].key === draggedNode.key;
const isDraggedNodeSameDepthAndBelow = isSameDepthAndJustBelow(
node,
draggedNode
);
// We want to allow dropping below if the event has no children OR if the only
// child of the event is the dragged one.
const canDropBelow =
!!node.event && (node.children.length === 0 || isDraggedNodesOnlyChild);
const canHaveSubEvents = !!node.event && node.event.canHaveSubEvents();
const indentWidth = getIndentWidth(windowWidth);
const dropAreaStyles = getTargetPositionStyles(
indentWidth,
draggedNodeHeight,
isDraggedNodesOnlyChild
);
const indicatorStyles = getIndicatorPositionStyles(indentWidth);
const commonProps = {
DnDComponent: DnDComponent,
canDrop: () => true,
};
return (
<div
style={{
visibility: activateTargets ? 'visible' : 'hidden',
}}
>
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['top'],
dropArea: dropAreaStyles['top'],
}}
onDrop={() => onDrop(moveNodeAbove, node)}
{...commonProps}
/>
{canHaveSubEvents && canDropBelow && (
<>
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom-right'],
dropArea: dropAreaStyles['bottom-right'],
}}
onDrop={() => onDrop(moveNodeAsSubEvent, node)}
{...commonProps}
/>
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom'],
dropArea: dropAreaStyles['bottom-left'],
}}
onDrop={() => onDrop(moveNodeBelow, node)}
{...commonProps}
/>
{/* Allow dragging left/right to move below or as subevent */}
{(isDraggedNodesOnlyChild || isDraggedNodeSameDepthAndBelow) && (
<>
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom'],
dropArea: dropAreaStyles['below-left'],
}}
onDrop={() => onDrop(moveNodeBelow, node)}
{...commonProps}
/>
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom-right'],
dropArea: dropAreaStyles['below-right'],
}}
onDrop={() => onDrop(moveNodeAsSubEvent, node)}
{...commonProps}
/>
</>
)}
</>
)}
{!canHaveSubEvents && canDropBelow && (
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom'],
dropArea: dropAreaStyles['bottom'],
}}
onDrop={() => onDrop(moveNodeBelow, node)}
{...commonProps}
/>
)}
{canHaveSubEvents && !canDropBelow && (
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom-right'],
dropArea: dropAreaStyles['bottom'],
}}
onDrop={() => onDrop(moveNodeAsSubEvent, node)}
{...commonProps}
/>
)}
</div>
);
}
export function AutoScroll({
direction,
DnDComponent,
activateTargets,
onHover,
}: {|
direction: 'top' | 'bottom',
DnDComponent: DropTargetComponent<SortableTreeNode>,
activateTargets: boolean,
onHover: () => void,
|}) {
const delayActivationTimer = React.useRef<?TimeoutID>(null);
const [show, setShow] = React.useState(false);
// This drop target overlaps with sibling drag source and cancels drag immediately.
// See: https://github.com/react-dnd/react-dnd/issues/766#issuecomment-388943403
// Delaying the render of the drop target seems to solve the issue.
React.useEffect(
() => {
if (activateTargets) {
delayActivationTimer.current = setTimeout(() => {
setShow(true);
}, 100);
} else {
setShow(false);
clearTimeout(delayActivationTimer.current);
delayActivationTimer.current = null;
}
return () => {
delayActivationTimer.current &&
clearTimeout(delayActivationTimer.current);
};
},
[activateTargets]
);
return (
<DnDComponent
canDrop={() => true}
drop={() => {
return;
}}
>
{({ isOverLazy, connectDropTarget }) => {
if (isOverLazy) {
onHover();
}
const dropTarget = (
<div
style={{
...sharedStyles.autoScroll,
...(direction === 'top' ? { top: 0 } : { bottom: 0 }),
// Uncomment for debugging purposes.
// backgroundColor: 'black',
// opacity: isOverLazy ? 1 : 0,
}}
/>
);
return show ? connectDropTarget(dropTarget) : null;
}}
</DnDComponent>
);
}

View File

@@ -0,0 +1,89 @@
import { type SortableTreeNode } from '.';
export type MoveFunctionArguments = {
targetNode: SortableTreeNode,
node: SortableTreeNode,
};
export const moveEventToEventsList = ({
targetEventsList,
movingEvent,
initialEventsList,
toIndex,
}: {
targetEventsList: gdEventsList,
movingEvent: gdBaseEvent,
initialEventsList: gdEventsList,
toIndex: number,
}) => {
initialEventsList.moveEventToAnotherEventsList(
movingEvent,
targetEventsList,
toIndex
);
};
export const moveNodeAsSubEvent = ({
targetNode,
node,
}: MoveFunctionArguments) => {
if (!targetNode.event || !node.event) return;
moveEventToEventsList({
targetEventsList: targetNode.event.getSubEvents(),
movingEvent: node.event,
initialEventsList: node.eventsList,
toIndex: 0,
});
};
export const moveNodeBelow = ({ targetNode, node }: MoveFunctionArguments) => {
if (!targetNode.event || !node.event) return;
const toIndex =
node.depth === targetNode.depth && node.indexInList < targetNode.indexInList
? targetNode.indexInList
: targetNode.indexInList + 1;
moveEventToEventsList({
targetEventsList: targetNode.eventsList,
movingEvent: node.event,
initialEventsList: node.eventsList,
toIndex,
});
};
export const moveNodeAbove = ({ targetNode, node }: MoveFunctionArguments) => {
if (!targetNode.event || !node.event) return;
const toIndex =
node.depth === targetNode.depth && node.indexInList < targetNode.indexInList
? targetNode.indexInList - 1
: targetNode.indexInList;
moveEventToEventsList({
targetEventsList: targetNode.eventsList,
movingEvent: node.event,
initialEventsList: node.eventsList,
toIndex,
});
};
export const isDescendant = (
olderNode: SortableTreeNode,
childNode: SortableTreeNode
) => {
if (childNode.depth <= olderNode.depth) return false;
const parentPath = olderNode.nodePath;
const childPath = childNode.nodePath;
return parentPath.every((pathValue, index) => pathValue === childPath[index]);
};
export const isSameDepthAndJustBelow = (
aboveNode: SortableTreeNode,
belowNode: SortableTreeNode
) => {
if (aboveNode.depth !== belowNode.depth) return false;
const belowNodePath = belowNode.nodePath;
const aboveNodePath = aboveNode.nodePath;
if (belowNodePath[belowNodePath.length - 1] === 0) return false;
return (
belowNodePath[belowNodePath.length - 1] - 1 ===
aboveNodePath[aboveNodePath.length - 1]
);
};

View File

@@ -2,16 +2,19 @@
import { Trans } from '@lingui/macro';
import React, { Component, type Node } from 'react';
import findIndex from 'lodash/findIndex';
import {
SortableTreeWithoutDndContext,
getNodeAtPath,
} from 'react-sortable-tree';
import { SortableTreeWithoutDndContext } 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, icon } from './ClassNames';
import {
eventsTree,
eventsTreeWithSearchResults,
handle,
icon,
} from './ClassNames';
import {
type SelectionState,
type EventContext,
@@ -37,8 +40,14 @@ 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';
const gd: libGDevelop = global.gd;
const eventsSheetEventsDnDType = 'events-sheet-events-dnd-type';
const getThumbnail = ObjectsRenderingService.getThumbnail.bind(
ObjectsRenderingService
);
@@ -47,16 +56,24 @@ const defaultIndentWidth = 22;
const smallIndentWidth = 11;
const styles = {
container: { flex: 1 },
container: { flex: 1, position: 'relative' },
defaultEventContainer: {
marginRight: 10,
position: 'relative',
},
smallEventContainer: {
marginRight: 0,
position: 'relative',
},
eventComponentContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'stretch',
position: 'relative',
},
};
const getIndentWidth = (windowWidth: WidthType) =>
export const getIndentWidth = (windowWidth: WidthType) =>
windowWidth === 'small' ? smallIndentWidth : defaultIndentWidth;
const getEventContainerStyle = (windowWidth: WidthType) =>
windowWidth === 'small'
@@ -95,8 +112,9 @@ type EventsContainerProps = {|
renderObjectThumbnail: string => Node,
screenType: ScreenType,
windowWidth: WidthType,
eventsSheetHeight: number,
connectDragSource: ConnectDragSource,
|};
/**
@@ -134,46 +152,49 @@ class EventContainer extends Component<EventsContainerProps, {||}> {
ref={container => (this._container = container)}
onClick={this.props.onEventClick}
onContextMenu={this._onEventContextMenu}
style={getEventContainerStyle(this.props.windowWidth)}
>
{EventComponent && (
<EventComponent
project={project}
scope={scope}
event={event}
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
selected={isEventSelected(this.props.selection, event)}
selection={this.props.selection}
leftIndentWidth={this.props.leftIndentWidth}
onUpdate={this._onEventUpdated}
onAddNewInstruction={this.props.onAddNewInstruction}
onPasteInstructions={this.props.onPasteInstructions}
onMoveToInstruction={this.props.onMoveToInstruction}
onMoveToInstructionsList={this.props.onMoveToInstructionsList}
onInstructionClick={this.props.onInstructionClick}
onInstructionDoubleClick={this.props.onInstructionDoubleClick}
onInstructionContextMenu={this.props.onInstructionContextMenu}
onAddInstructionContextMenu={this.props.onAddInstructionContextMenu}
onParameterClick={this.props.onParameterClick}
onOpenExternalEvents={this.props.onOpenExternalEvents}
onOpenLayout={this.props.onOpenLayout}
disabled={
disabled /* Use disabled (not event.disabled) as it is true if a parent event is disabled*/
}
renderObjectThumbnail={this.props.renderObjectThumbnail}
screenType={this.props.screenType}
windowWidth={this.props.windowWidth}
eventsSheetHeight={this.props.eventsSheetHeight}
/>
<div style={styles.eventComponentContainer}>
{this.props.connectDragSource(<div className={handle} />)}
<div style={styles.container}>
<EventComponent
project={project}
scope={scope}
event={event}
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
selected={isEventSelected(this.props.selection, event)}
selection={this.props.selection}
leftIndentWidth={this.props.leftIndentWidth}
onUpdate={this._onEventUpdated}
onAddNewInstruction={this.props.onAddNewInstruction}
onPasteInstructions={this.props.onPasteInstructions}
onMoveToInstruction={this.props.onMoveToInstruction}
onMoveToInstructionsList={this.props.onMoveToInstructionsList}
onInstructionClick={this.props.onInstructionClick}
onInstructionDoubleClick={this.props.onInstructionDoubleClick}
onInstructionContextMenu={this.props.onInstructionContextMenu}
onAddInstructionContextMenu={
this.props.onAddInstructionContextMenu
}
onParameterClick={this.props.onParameterClick}
onOpenExternalEvents={this.props.onOpenExternalEvents}
onOpenLayout={this.props.onOpenLayout}
disabled={
disabled /* Use disabled (not event.disabled) as it is true if a parent event is disabled*/
}
renderObjectThumbnail={this.props.renderObjectThumbnail}
screenType={this.props.screenType}
eventsSheetHeight={this.props.eventsSheetHeight}
/>
</div>
</div>
)}
</div>
);
}
}
const getNodeKey = ({ treeIndex }) => treeIndex;
const SortableTree = ({ className, ...otherProps }) => (
<ThemeConsumer>
{muiTheme => (
@@ -238,36 +259,64 @@ type EventsTreeProps = {|
|};
// A node displayed by the SortableTree. Almost always represents an
// event, except for the buttons at the bottom of the sheet.
type SortableTreeNode = {
// 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,
nodePath: Array<number>,
// 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,
|};
/**
* 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, *> {
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;
constructor(props: EventsTreeProps) {
super(props);
this.temporaryUnfoldedNodes = [];
this.eventsHeightsCache = new EventHeightsCache(this);
this.state = {
...this._eventsToTreeData(props.events),
draggedNode: null,
isScrolledTop: true,
isScrolledBottom: false,
};
}
@@ -295,6 +344,10 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
}
}
componentWillUnmount() {
this._hoverTimerId && clearTimeout(this._hoverTimerId);
}
/**
* Should be called whenever an event height has changed
*/
@@ -359,8 +412,10 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
eventsList: gdEventsList,
flatData: Array<gdBaseEvent> = [],
depth: number = 0,
parentDisabled: boolean = false
parentDisabled: boolean = false,
path: Array<number> = []
) => {
let eventIndexInFlattenedTree = 0;
const treeData = mapFor<SortableTreeNode>(
0,
eventsList.getEventsCount(),
@@ -369,6 +424,8 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
flatData.push(event);
const disabled = parentDisabled || event.isDisabled();
const currentPath = path.concat(eventIndexInFlattenedTree);
eventIndexInFlattenedTree += 1;
return {
title: this._renderEvent,
@@ -385,8 +442,10 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
// Hence it should not contain the folded events.
!event.isFolded() ? flatData : [],
depth + 1,
disabled
disabled,
currentPath
).treeData,
nodePath: currentPath,
};
}
);
@@ -405,6 +464,9 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
onAddEvent={(eventType: string) =>
this.props.onAddNewEvent(eventType, this.props.events)
}
DnDComponent={this.DropTarget}
draggedNode={this.state.draggedNode}
rootEventsList={eventsList}
/>
),
event: null,
@@ -459,76 +521,31 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
].filter(Boolean);
return {
// $FlowFixMe - We are confident treeData and extraNodes are both arrays of SortableTreeNode
treeData: extraNodes.length ? treeData.concat(extraNodes) : treeData,
flatData,
};
};
_onMoveNode = ({
treeData,
path,
node,
}: {
treeData: any,
path: Array<any>,
node: SortableTreeNode,
}) => {
// Get the moved event and its list from the moved node.
const { event, eventsList } = node;
if (!event) return;
// Get the event list where the event should be moved to.
const targetPath = path.slice(0, -1);
const target = getNodeAtPath({
getNodeKey,
treeData: treeData,
path: targetPath,
});
const targetNode = target.node;
const targetEventsList =
targetNode && targetNode.event
? targetNode.event.getSubEvents()
: this.props.events;
const targetPosition =
targetNode && targetNode.children ? targetNode.children.indexOf(node) : 0;
// Do the move
// Note that moveEventToAnotherEventsList does not invalidate the
// references to the event in memory - so things refering to this event like the
// selection in EventsSheet remain valid. This might not be needed anymore
// if events drag'n'drop is reworked to be similar to instructions drag'n'drop.
eventsList.moveEventToAnotherEventsList(
event,
targetEventsList,
targetPosition
);
this.forceEventsUpdate();
this.props.onEventMoved();
};
_canDrag = ({ node }: { node: ?SortableTreeNode }) => {
_canDrag = (node: ?SortableTreeNode) => {
return !!node && !!node.event;
};
_canDrop = ({ nextParent }: { nextParent: ?SortableTreeNode }) => {
if (nextParent) {
if (nextParent.event) {
return nextParent.event.canHaveSubEvents();
}
}
// No "nextParent" means that we're trying to drop at the root
// of the events tree.
_canDrop = (hoveredNode: SortableTreeNode) => {
return true;
};
_canNodeHaveChildren = (node: ?SortableTreeNode) => {
if (node && node.event) {
return node.event.canHaveSubEvents();
_onDrop = (
moveFunction: MoveFunctionArguments => void,
currentNode: SortableTreeNode
) => {
if (this.state.draggedNode) {
moveFunction({
node: this.state.draggedNode,
targetNode: currentNode,
});
}
return false;
this.props.onEventMoved();
};
_onVisibilityToggle = ({ node }: { node: SortableTreeNode }) => {
@@ -557,54 +574,148 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
);
};
_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.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 (
<EventContainer
project={this.props.project}
scope={this.props.scope}
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
event={event}
key={event.ptr}
eventsHeightsCache={this.eventsHeightsCache}
selection={this.props.selection}
leftIndentWidth={depth * getIndentWidth(this.props.windowWidth)}
onAddNewInstruction={this.props.onAddNewInstruction}
onPasteInstructions={this.props.onPasteInstructions}
onMoveToInstruction={this.props.onMoveToInstruction}
onMoveToInstructionsList={this.props.onMoveToInstructionsList}
onInstructionClick={this.props.onInstructionClick}
onInstructionDoubleClick={this.props.onInstructionDoubleClick}
onParameterClick={this.props.onParameterClick}
onEventClick={() =>
this.props.onEventClick({
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
})
}
onEventContextMenu={(x, y) =>
this.props.onEventContextMenu(x, y, {
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
})
}
onInstructionContextMenu={this.props.onInstructionContextMenu}
onAddInstructionContextMenu={this.props.onAddInstructionContextMenu}
onOpenExternalEvents={this.props.onOpenExternalEvents}
onOpenLayout={this.props.onOpenLayout}
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}
windowWidth={this.props.windowWidth}
eventsSheetHeight={this.props.eventsSheetHeight}
/>
<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 dropTarget = (
<div
style={{
opacity: isDragged ? 0.5 : 1,
...getEventContainerStyle(this.props.windowWidth),
}}
>
<EventContainer
project={this.props.project}
scope={this.props.scope}
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
event={event}
key={event.ptr}
eventsHeightsCache={this.eventsHeightsCache}
selection={this.props.selection}
leftIndentWidth={depth * getIndentWidth(this.props.windowWidth)}
onAddNewInstruction={this.props.onAddNewInstruction}
onPasteInstructions={this.props.onPasteInstructions}
onMoveToInstruction={this.props.onMoveToInstruction}
onMoveToInstructionsList={this.props.onMoveToInstructionsList}
onInstructionClick={this.props.onInstructionClick}
onInstructionDoubleClick={this.props.onInstructionDoubleClick}
onParameterClick={this.props.onParameterClick}
onEventClick={() =>
this.props.onEventClick({
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
})
}
onEventContextMenu={(x, y) =>
this.props.onEventContextMenu(x, y, {
eventsList: node.eventsList,
event: event,
indexInList: node.indexInList,
})
}
onInstructionContextMenu={this.props.onInstructionContextMenu}
onAddInstructionContextMenu={
this.props.onAddInstructionContextMenu
}
onOpenExternalEvents={this.props.onOpenExternalEvents}
onOpenLayout={this.props.onOpenLayout}
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}
eventsSheetHeight={this.props.eventsSheetHeight}
connectDragSource={connectDragSource}
/>
{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}
windowWidth={this.props.windowWidth}
/>
)}
</div>
);
return isDragged ? dropTarget : connectDropTarget(dropTarget);
}}
</DragSourceAndDropTarget>
);
};
@@ -625,6 +736,14 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
);
};
_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
@@ -641,20 +760,35 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
'--icon-size': `${Math.round(zoomLevel * 1.14)}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.windowWidth)}
onChange={noop}
onVisibilityToggle={this._onVisibilityToggle}
onMoveNode={this._onMoveNode}
canDrag={this._canDrag}
canDrop={this._canDrop}
canNodeHaveChildren={this._canNodeHaveChildren}
rowHeight={({ node }: { node: SortableTreeNode }) => {
if (!node.event) return node.fixedHeight || 0;
return this.eventsHeightsCache.getEventHeight(node.event);
}}
canDrag={false}
rowHeight={this._getRowHeight}
searchMethod={this._isNodeHighlighted}
searchQuery={this.props.searchResults}
searchFocusOffset={this.props.searchFocusOffset}
@@ -663,8 +797,19 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
}
reactVirtualizedListProps={{
ref: list => (this._list = list),
onScroll: this.props.onScroll,
onScroll: event => {
this.props.onScroll && this.props.onScroll();
this.setState({
isScrolledTop: event.scrollTop === 0,
isScrolledBottom:
event.clientHeight + event.scrollTop >= event.scrollHeight,
});
},
}}
// Disable slideRegionSize on touchscreen because of a bug that makes scrolling
// uncontrollable on touchscreens. Ternary operator does not update slideRegionSize
// well.
slideRegionSize={-10}
/>
</div>
);

View File

@@ -9,11 +9,12 @@
/**
* Draggable handle on the left of an event
*/
.gd-events-sheet .rst__moveHandle {
.gd-events-sheet .move-handle {
width: 10px;
border: none;
box-shadow: none;
border-radius: 0;
margin: 0.5px;
flex-grow: 0;
flex-shrink: 0;
cursor: move;
}
@media only screen and (max-width: 750px) {
@@ -21,7 +22,7 @@
* Slightly reduce the size of the handle on small screens
* and phones (where drag'n'drop is not supported anyway).
*/
.gd-events-sheet .rst__moveHandle {
.gd-events-sheet .move-handle {
width: 4px;
}
}

View File

@@ -6,7 +6,7 @@ const styles = {
columnDropIndicator: {
borderRight: '1px solid',
borderLeft: '1px solid',
width: 7,
width: 8,
marginLeft: '-1px',
height: '100%',
boxSizing: 'border-box',
@@ -19,8 +19,8 @@ export function ColumnDropIndicator() {
<div
style={{
...styles.columnDropIndicator,
backgroundColor: gdevelopTheme.closableTabs.selectedBackgroundColor,
borderColor: gdevelopTheme.closableTabs.backgroundColor,
backgroundColor: gdevelopTheme.dropIndicator.canDrop,
borderColor: gdevelopTheme.dropIndicator.border,
}}
/>
);

View File

@@ -16,12 +16,14 @@ type Props<DraggedItemType> = {|
connectDragSource: ConnectDragSource,
connectDropTarget: ConnectDropTarget,
isOver: boolean,
isOverLazy: boolean,
canDrop: boolean,
}) => ?React.Node,
beginDrag: () => DraggedItemType,
canDrag?: (item: DraggedItemType) => boolean,
canDrop: (item: DraggedItemType) => boolean,
drop: () => void,
endDrag?: () => void,
|};
type DragSourceProps = {|
@@ -31,6 +33,7 @@ type DragSourceProps = {|
type DropTargetProps = {|
connectDropTarget: ConnectDropTarget,
isOver: boolean,
isOverLazy: boolean,
canDrop: boolean,
|};
@@ -53,6 +56,9 @@ export const makeDragSourceAndDropTarget = <DraggedItemType>(
beginDrag(props: InnerDragSourceAndDropTargetProps<DraggedItemType>) {
return props.beginDrag();
},
endDrag(props: Props<DraggedItemType>, monitor: DragSourceMonitor) {
if (props.endDrag) props.endDrag();
},
};
function sourceCollect(
@@ -84,6 +90,7 @@ export const makeDragSourceAndDropTarget = <DraggedItemType>(
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({ shallow: true }),
isOverLazy: monitor.isOver({ shallow: false }),
canDrop: monitor.canDrop(),
};
}
@@ -94,11 +101,19 @@ export const makeDragSourceAndDropTarget = <DraggedItemType>(
sourceCollect
)(
DropTarget(reactDndType, targetSpec, targetCollect)(
({ children, connectDragSource, connectDropTarget, isOver, canDrop }) => {
({
children,
connectDragSource,
connectDropTarget,
isOver,
isOverLazy,
canDrop,
}) => {
return children({
connectDragSource,
connectDropTarget,
isOver,
isOverLazy,
canDrop,
});
}

View File

@@ -11,6 +11,7 @@ type Props<DraggedItemType> = {|
children: ({
connectDropTarget: ConnectDropTarget,
isOver: boolean,
isOverLazy: boolean,
canDrop: boolean,
}) => ?React.Node,
canDrop: (item: DraggedItemType) => boolean,
@@ -18,15 +19,20 @@ type Props<DraggedItemType> = {|
drop: (monitor: DropTargetMonitor) => void,
|};
export type DropTargetComponent<DraggedItemType> = (
Props<DraggedItemType>
) => React.Node;
type DropTargetProps = {|
connectDropTarget: ConnectDropTarget,
isOver: boolean,
isOverLazy: boolean,
canDrop: boolean,
|};
export const makeDropTarget = <DraggedItemType>(
reactDndType: string
): ((Props<DraggedItemType>) => React.Node) => {
): DropTargetComponent<DraggedItemType> => {
const targetSpec = {
canDrop(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
const item = monitor.getItem();
@@ -50,15 +56,17 @@ export const makeDropTarget = <DraggedItemType>(
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({ shallow: true }),
isOverLazy: monitor.isOver({ shallow: false }),
canDrop: monitor.canDrop(),
};
}
const InnerDropTarget = DropTarget(reactDndType, targetSpec, targetCollect)(
({ children, connectDropTarget, isOver, canDrop }) => {
({ children, connectDropTarget, isOver, isOverLazy, canDrop }) => {
return children({
connectDropTarget,
isOver,
isOverLazy,
canDrop,
});
}

View File

@@ -20,8 +20,8 @@ export default function DropIndicator({ canDrop }: {| canDrop: boolean |}) {
style={{
...styles.dropIndicator,
borderColor: canDrop
? gdevelopTheme.listItem.selectedBackgroundColor
: gdevelopTheme.listItem.selectedErrorBackgroundColor,
? gdevelopTheme.dropIndicator.canDrop
: gdevelopTheme.dropIndicator.cannotDrop,
}}
/>
);

View File

@@ -219,6 +219,11 @@ export function createGdevelopTheme(
backgroundColor: styles['ThemeTextHighlightedBackgroundColor'],
},
},
dropIndicator: {
canDrop: styles['ThemeDropIndicatorCanDropColor'],
cannotDrop: styles['ThemeDropIndicatorCannotDropColor'],
border: styles['ThemeDropIndicatorBorderColor'],
},
closableTabs: {
fontFamily: styles['GdevelopFontFamily'],
containerBackgroundColor: styles['ThemeSurfaceWindowBackgroundColor'],

View File

@@ -165,6 +165,23 @@
"frame-border-color": {
"value": "#dddddd"
}
},
"drop-indicator": {
"can-drop": {
"color": {
"value": "{gdevelop.color.dark-blue.value}"
}
},
"cannot-drop": {
"color": {
"value": "#ff5c5c"
}
},
"border": {
"color": {
"value": "{theme.surface.canvas.background-color.value}"
}
}
}
},
"input": {

View File

@@ -166,6 +166,23 @@
"frame-border-color": {
"value": "#000000"
}
},
"drop-indicator": {
"can-drop": {
"color": {
"value": "{gdevelop.color.light-blue.value}"
}
},
"cannot-drop": {
"color": {
"value": "#ff5c5c"
}
},
"border": {
"color": {
"value": "{theme.surface.canvas.background-color.value}"
}
}
}
},
"input": {

View File

@@ -4,10 +4,10 @@
*/
.gd-events-sheet .events-tree {
/* Use the fonts provided by the operating system(s) as possible. */
/* If you update this font list, be sure to do it in all the other places using fonts in the codebase. */
font-family: var(--gdevelop-font-family) !important;
background: var(--event-sheet-event-tree-background-color);
/* Use the fonts provided by the operating system(s) as possible. */
/* If you update this font list, be sure to do it in all the other places using fonts in the codebase. */
font-family: var(--gdevelop-font-family) !important;
background: var(--event-sheet-event-tree-background-color);
}
/**
@@ -15,18 +15,18 @@
* of horizontal scrollbar.
*/
.gd-events-sheet .rst__virtualScrollOverride {
overflow-x: hidden !important;
overflow-x: hidden !important;
}
/**
* Draggable handle on the left of an event
*/
.gd-events-sheet .rst__moveHandle {
background: var(--event-sheet-rst-move-handle-background-color);
.gd-events-sheet .move-handle {
background: var(--event-sheet-rst-move-handle-background-color);
}
.gd-events-sheet .rst__moveHandle:hover {
background-color: var(--event-sheet-rst-move-handle-hover-background-color);
.gd-events-sheet .move-handle:hover {
background-color: var(--event-sheet-rst-move-handle-hover-background-color);
}
/**
@@ -36,89 +36,89 @@
.gd-events-sheet .rst__lineFullVertical::after,
.gd-events-sheet .rst__lineHalfVerticalTop::after,
.gd-events-sheet .rst__lineHalfVerticalBottom::after {
background-color: var(--event-sheet-rst-line-background-color);
background-color: var(--event-sheet-rst-line-background-color);
}
/**
* Background and default text color of content of an event
*/
.gd-events-sheet .rst__rowContents {
background-color: var(--event-sheet-rst-row-contents-background-color);
color: var(--event-sheet-rst-row-contents-color);
background-color: var(--event-sheet-rst-row-contents-background-color);
color: var(--event-sheet-rst-row-contents-color);
}
/**
* Selectable area (instructions)
*/
.gd-events-sheet .selectable:hover {
background-color: var(--event-sheet-selectable-background-color);
background-color: var(--event-sheet-selectable-background-color);
}
.gd-events-sheet .selectable.selected {
background-color: var(--event-sheet-selectable-background-color);
border: var(--event-sheet-selectable-selected-border) !important;
background-color: var(--event-sheet-selectable-background-color);
border: var(--event-sheet-selectable-selected-border) !important;
}
/**
* Large selectable area (events)
*/
.gd-events-sheet .large-selectable {
border: var(--event-sheet-selectable-border) !important;
border: var(--event-sheet-selectable-border) !important;
}
.gd-events-sheet .large-selectable.large-selected {
border: var(--event-sheet-selectable-selected-border) !important;
border: var(--event-sheet-selectable-selected-border) !important;
}
/**
* Conditions and actions containers
*/
.gd-events-sheet .conditions-container {
background: var(--event-sheet-conditions-background-color);
border: var(--event-sheet-conditions-border);
color: var(--event-sheet-conditions-color);
border-right-color: var(--event-sheet-conditions-border-right-color);
padding-left: 5px;
padding-right: 5px;
/* Prevent container to growing outside its parent - and still don't have a ridiculously small size */
min-width: 100px;
background: var(--event-sheet-conditions-background-color);
border: var(--event-sheet-conditions-border);
color: var(--event-sheet-conditions-color);
border-right-color: var(--event-sheet-conditions-border-right-color);
padding-left: 5px;
padding-right: 5px;
/* Prevent container to growing outside its parent - and still don't have a ridiculously small size */
min-width: 100px;
}
.gd-events-sheet .conditions-container.small-width-container {
border-right-color: var(--event-sheet-conditions-border-color);
border-bottom-color: var(--event-sheet-conditions-border-color);
border-right-color: var(--event-sheet-conditions-border-color);
border-bottom-color: var(--event-sheet-conditions-border-color);
}
.gd-events-sheet .actions-container {
background: var(--event-sheet-actions-background-color);
color: var(--event-sheet-actions-color);
padding-left: 5px;
padding-right: 5px;
/* Prevent container to growing outside its parent. We don't have a way to prevent an infinitely
background: var(--event-sheet-actions-background-color);
color: var(--event-sheet-actions-color);
padding-left: 5px;
padding-right: 5px;
/* Prevent container to growing outside its parent. We don't have a way to prevent an infinitely
* small action column, but responsive design should avoid the problem on small screens. */
min-width: 0;
min-width: 0;
}
.gd-events-sheet .actions-container.small-width-container {
padding-left: 20px;
padding-left: 20px;
}
.gd-events-sheet .sub-instructions-container {
margin-left: 9px;
margin-top: 1px;
border-right: none;
border-left: var(--event-sheet-sub-instructions-border);
margin-left: 9px;
margin-top: 1px;
border-right: none;
border-left: var(--event-sheet-sub-instructions-border);
}
/**
* Instructions parameters color scheme
*/
.gd-events-sheet .instruction-parameter {
color: var(--event-sheet-instruction-parameter-base-color);
color: var(--event-sheet-instruction-parameter-base-color);
}
.gd-events-sheet .instruction-parameter.expression {
color: var(--event-sheet-instruction-parameter-expression-color);
color: var(--event-sheet-instruction-parameter-expression-color);
}
.gd-events-sheet .instruction-parameter.object,
@@ -126,112 +126,112 @@
.gd-events-sheet .instruction-parameter.objectList,
.gd-events-sheet .instruction-parameter.objectListOrEmptyIfJustDeclared,
.gd-events-sheet .instruction-parameter.objectListOrEmptyWithoutPicking {
color:var(--event-sheet-instruction-parameter-object-color);
font-weight: bold;
color:var(--event-sheet-instruction-parameter-object-color);
font-weight: bold;
}
.gd-events-sheet .instruction-parameter.behavior {
color: var(--event-sheet-instruction-parameter-behavior-color);
color: var(--event-sheet-instruction-parameter-behavior-color);
}
.gd-events-sheet .instruction-parameter.operator {
color: var(--event-sheet-instruction-parameter-operator-color);
color: var(--event-sheet-instruction-parameter-operator-color);
}
.gd-events-sheet .instruction-parameter.objectvar {
color: var(--event-sheet-instruction-parameter-var-color);
color: var(--event-sheet-instruction-parameter-var-color);
}
.gd-events-sheet .instruction-parameter.scenevar {
color: var(--event-sheet-instruction-parameter-var-color);
color: var(--event-sheet-instruction-parameter-var-color);
}
.gd-events-sheet .instruction-parameter.globalvar {
color: var(--event-sheet-instruction-parameter-var-color);
color: var(--event-sheet-instruction-parameter-var-color);
}
.gd-events-sheet .instruction-parameter .instruction-invalid-parameter {
color: var(--event-sheet-instruction-parameter-error-color);
text-decoration: var(--event-sheet-instruction-parameter-error-color) underline wavy;
color: var(--event-sheet-instruction-parameter-error-color);
text-decoration: var(--event-sheet-instruction-parameter-error-color) underline wavy;
}
.gd-events-sheet .instruction-parameter .instruction-warning-parameter {
color: var(--event-sheet-instruction-parameter-warning-color);
text-decoration: var(--event-sheet-instruction-parameter-warning-color) underline wavy;
color: var(--event-sheet-instruction-parameter-warning-color);
text-decoration: var(--event-sheet-instruction-parameter-warning-color) underline wavy;
}
.gd-events-sheet .instruction-parameter .instruction-missing-parameter {
display: inline-flex;
background-color: var(--event-sheet-instruction-parameter-error-background-color);
border-bottom: 1px dashed var(--event-sheet-instruction-parameter-error-color);
min-width: 25px;
min-height: 14px;
display: inline-flex;
background-color: var(--event-sheet-instruction-parameter-error-background-color);
border-bottom: 1px dashed var(--event-sheet-instruction-parameter-error-color);
min-width: 25px;
min-height: 14px;
}
/**
* Search results highlight
*/
.gd-events-sheet .with-search-results .rst__rowSearchMatch {
outline: none;
outline: none;
}
.gd-events-sheet .with-search-results .rst__rowSearchMatch .rst__moveHandle {
background: var(--event-sheet-events-highlighted-color);
.gd-events-sheet .with-search-results .rst__rowSearchMatch .move-handle {
background: var(--event-sheet-events-highlighted-color);
}
.gd-events-sheet .with-search-results .rst__rowSearchMatch .rst__moveHandle:hover {
filter: brightness(120%);
.gd-events-sheet .with-search-results .rst__rowSearchMatch .move-handle:hover {
filter: brightness(120%);
}
.gd-events-sheet .with-search-results .rst__rowSearchMatch .conditions-container {
outline: none;
background-color: var(--event-sheet-events-highlighted-background-color);
outline: none;
background-color: var(--event-sheet-events-highlighted-background-color);
}
.gd-events-sheet .with-search-results .rst__rowSearchMatch .actions-container {
outline: none;
background-color: var(--event-sheet-events-highlighted-background-color);
outline: none;
background-color: var(--event-sheet-events-highlighted-background-color);
}
.gd-events-sheet .with-search-results .rst__rowSearchFocus .rst__moveHandle {
width: 15px;
.gd-events-sheet .with-search-results .rst__rowSearchFocus .move-handle {
width: 15px;
}
/**
* Drag'n'drop indicator (events)
* Drag'n'drop indicator (events - instructions)
*/
.gd-events-sheet .drop-indicator {
border-top: 2px solid var(--event-sheet-drop-indicator-can-drop-border-top-color);
height: 0;
margin-top: -1px;
margin-bottom: -1px;
width: 100%;
box-sizing: border-box;
border-top: 2px solid var(--event-sheet-drop-indicator-can-drop-border-top-color);
height: 0;
margin-top: -1px;
margin-bottom: -1px;
width: 100%;
box-sizing: border-box;
}
.gd-events-sheet .cant-drop-indicator {
border-top: 2px solid var(--event-sheet-drop-indicator-can-drop-border-top-color);
height: 0;
margin-top: -1px;
margin-bottom: -1px;
width: 100%;
box-sizing: border-box;
border-top: 2px solid var(--event-sheet-drop-indicator-can-drop-border-top-color);
height: 0;
margin-top: -1px;
margin-bottom: -1px;
width: 100%;
box-sizing: border-box;
}
/**
* Skinning the "Link" event
*/
.gd-events-sheet .link-container {
background: var(--event-sheet-link-container-background-color);
background: var(--event-sheet-link-container-background-color);
}
.gd-events-sheet-dark-theme .link-container span {
color: var(--event-sheet-link-container-color);
color: var(--event-sheet-link-container-color);
}
.gd-events-sheet .link-container .selectable {
color: var(--event-sheet-link-selectable-link-color);
font-weight: bold;
color: var(--event-sheet-link-selectable-link-color);
font-weight: bold;
}
/* Theming icons */
.gd-events-sheet .icon {
padding-right: 2px;
width: 16px;
height: 16px;
padding-right: 2px;
width: 16px;
height: 16px;
}

View File

@@ -165,6 +165,23 @@
"frame-border-color": {
"value": "#ECEFF4"
}
},
"drop-indicator": {
"can-drop": {
"color": {
"value": "{theme.primary.color.value}"
}
},
"cannot-drop": {
"color": {
"value": "#BF616A"
}
},
"border": {
"color": {
"value": "{theme.surface.canvas.background-color.value}"
}
}
}
},
"input": {

View File

@@ -175,6 +175,23 @@
"frame-border-color": {
"value": "#D6DEEC"
}
},
"drop-indicator": {
"can-drop": {
"color": {
"value": "#4C5361"
}
},
"cannot-drop": {
"color": {
"value": "#E06C75"
}
},
"border": {
"color": {
"value": "#21252B"
}
}
}
},
"input": {

View File

@@ -165,6 +165,23 @@
"frame-border-color": {
"value": "#dcdcdc"
}
},
"drop-indicator": {
"can-drop": {
"color": {
"value": "#047AA6"
}
},
"cannot-drop": {
"color": {
"value": "#BF616A"
}
},
"border": {
"color": {
"value": "{theme.surface.canvas.background-color.value}"
}
}
}
},
"input": {