[WIP] Add EventsTree

This commit is contained in:
Florian Rival
2017-07-30 09:16:51 +02:00
parent 31c741b8c6
commit 41f6d8164c
10 changed files with 457 additions and 20 deletions

View File

@@ -32,6 +32,7 @@
"react-measure": "1.4.6",
"react-mosaic-component": "^0.4.0",
"react-sortable-hoc": "^0.6.3",
"react-sortable-tree": "^0.1.21",
"react-tap-event-plugin": "2.0.1",
"react-virtualized": "9.3.0",
"slug": "0.9.1",

View File

@@ -165,6 +165,9 @@ class CommentEvent extends Component {
children.push(
React.createElement('p', {
key: 'p',
style: {
whiteSpace: 'pre-line',
},
dangerouslySetInnerHTML: {
__html: commentEvent
.getComment()

View File

@@ -1,6 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import EventsList from './EventsList';
import EventsTree from './EventsTree';
const styles = {
container: {
@@ -23,10 +24,7 @@ export default class EventsSheet extends Component {
render() {
return (
<div style={styles.container}>
<EventsList
eventsList={this.props.events}
callbacks={this.props.callbacks}
/>
<EventsTree events={this.props.events}/>
</div>
);
}

View File

@@ -139,9 +139,6 @@ export default class EventsSheetContainer extends BaseEditor {
onEditEventTemplate: () => console.log('onEditEventTemplate'),
}}
/>
<PlaceholderMessage>
This editor is not finished yet.
</PlaceholderMessage>
{this.state.newInstruction.instruction &&
<InstructionEditorDialog
{...this.state.newInstruction}

View File

@@ -0,0 +1,217 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import baseStyles from './node-renderer-default.scss';
import { isDescendant } from './utils/tree-data-utils';
let styles = baseStyles;
class NodeRendererDefault extends Component {
render() {
const {
scaffoldBlockPxWidth,
toggleChildrenVisibility,
connectDragPreview,
connectDragSource,
isDragging,
canDrop,
canDrag,
node,
draggedNode,
path,
treeIndex,
isSearchMatch,
isSearchFocus,
buttons,
className,
style,
didDrop,
/* eslint-disable no-unused-vars */
isOver: _isOver, // Not needed, but preserved for other renderers
parentNode: _parentNode, // Needed for drag-and-drop utils
endDrag: _endDrag, // Needed for drag-and-drop utils
startDrag: _startDrag, // Needed for drag-and-drop utils
/* eslint-enable no-unused-vars */
...otherProps
} = this.props;
let handle;
if (canDrag) {
if (typeof node.children === 'function' && node.expanded) {
// Show a loading symbol on the handle when the children are expanded
// and yet still defined by a function (a callback to fetch the children)
handle = (
<div className={styles.loadingHandle}>
<div className={styles.loadingCircle}>
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
<div className={styles.loadingCirclePoint} />
</div>
</div>
);
} else {
// Show the handle used to initiate a drag-and-drop
handle = connectDragSource(<div className={styles.moveHandle} />, {
dropEffect: 'copy',
});
}
}
const isDraggedDescendant = draggedNode && isDescendant(draggedNode, node);
const isLandingPadActive = !didDrop && isDragging;
return (
<div style={{ height: '100%' }} {...otherProps}>
{toggleChildrenVisibility &&
node.children &&
node.children.length > 0 &&
<div>
<button
type="button"
aria-label={node.expanded ? 'Collapse' : 'Expand'}
className={
node.expanded ? styles.collapseButton : styles.expandButton
}
style={{ left: -0.5 * scaffoldBlockPxWidth }}
onClick={() =>
toggleChildrenVisibility({
node,
path,
treeIndex,
})}
/>
{node.expanded &&
!isDragging &&
<div
style={{ width: scaffoldBlockPxWidth }}
className={styles.lineChildren}
/>}
</div>}
<div className={styles.rowWrapper}>
{/* Set the row preview to be used during drag and drop */}
{connectDragPreview(
<div
className={
styles.row +
(isLandingPadActive ? ` ${styles.rowLandingPad}` : '') +
(isLandingPadActive && !canDrop
? ` ${styles.rowCancelPad}`
: '') +
(isSearchMatch ? ` ${styles.rowSearchMatch}` : '') +
(isSearchFocus ? ` ${styles.rowSearchFocus}` : '') +
(className ? ` ${className}` : '')
}
style={{
opacity: isDraggedDescendant ? 0.5 : 1,
...style,
}}
>
{handle}
<div
className={
styles.rowContents +
(!canDrag ? ` ${styles.rowContentsDragDisabled}` : '')
}
>
<div className={styles.rowLabel}>
<span
className={
styles.rowTitle +
(node.subtitle ? ` ${styles.rowTitleWithSubtitle}` : '')
}
>
{typeof node.title === 'function'
? node.title({
node,
path,
treeIndex,
})
: node.title}
</span>
{node.subtitle &&
<span className={styles.rowSubtitle}>
{typeof node.subtitle === 'function'
? node.subtitle({
node,
path,
treeIndex,
})
: node.subtitle}
</span>}
</div>
<div className={styles.rowToolbar}>
{buttons.map((btn, index) =>
<div
key={index} // eslint-disable-line react/no-array-index-key
className={styles.toolbarButton}
>
{btn}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}
}
NodeRendererDefault.defaultProps = {
isSearchMatch: false,
isSearchFocus: false,
canDrag: false,
toggleChildrenVisibility: null,
buttons: [],
className: '',
style: {},
parentNode: null,
draggedNode: null,
canDrop: false,
};
NodeRendererDefault.propTypes = {
node: PropTypes.shape({}).isRequired,
path: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
).isRequired,
treeIndex: PropTypes.number.isRequired,
isSearchMatch: PropTypes.bool,
isSearchFocus: PropTypes.bool,
canDrag: PropTypes.bool,
scaffoldBlockPxWidth: PropTypes.number.isRequired,
toggleChildrenVisibility: PropTypes.func,
buttons: PropTypes.arrayOf(PropTypes.node),
className: PropTypes.string,
style: PropTypes.shape({}),
// Drag and drop API functions
// Drag source
connectDragPreview: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
parentNode: PropTypes.shape({}), // Needed for drag-and-drop utils
startDrag: PropTypes.func.isRequired, // Needed for drag-and-drop utils
endDrag: PropTypes.func.isRequired, // Needed for drag-and-drop utils
isDragging: PropTypes.bool.isRequired,
didDrop: PropTypes.bool.isRequired,
draggedNode: PropTypes.shape({}),
// Drop target
isOver: PropTypes.bool.isRequired,
canDrop: PropTypes.bool,
};
export default NodeRendererDefault;

View File

@@ -0,0 +1,131 @@
import React, { Component } from 'react';
import {
SortableTreeWithoutDndContext as SortableTree,
getNodeAtPath,
} from 'react-sortable-tree';
import EventsRenderingService from '../EventsRenderingService';
import { mapFor } from '../../Utils/MapFor';
//TODO: Any change (event not equal to its last height or new event should trigger a recomputeRowHeights)
const eventHeights = {};
class EventRenderer extends Component {
componentDidMount() {
const height = this._container.clientHeight;
console.log(this.props.event.ptr, ' has height ', height);
eventHeights[this.props.event.ptr] = height;
}
render() {
const {event} = this.props;
const EventComponent = EventsRenderingService.getEventComponent(event);
return (
<div
ref={container => this._container = container}
>
{EventComponent && <EventComponent event={event}/>}
</div>
);
}
}
const renderEvent = ({ node }) => {
const event = node.event;
return <EventRenderer event={event} key={event.ptr}/>;
};
const toTreeData = (eventsList, flatData = []) => {
const treeData = mapFor(0, eventsList.getEventsCount(), i => {
const event = eventsList.getEventAt(i);
flatData.push(event);
return {
title: renderEvent,
event,
eventsList,
expanded: true,
key: event.ptr,
children: toTreeData(event.getSubEvents(), flatData).treeData,
};
});
return {
treeData,
flatData,
};
};
const getNodeKey = ({ treeIndex }) => treeIndex;
export default class Tree extends Component {
constructor(props) {
super(props);
this.state = {
...toTreeData(props.events),
};
}
componentDidMount() {
this._list.wrappedInstance.recomputeRowHeights();
this.forceUpdate();
}
_onMoveNode = ({ treeData, path, node }) => {
// 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;
// Get the moved event and its list from the moved node.
const { event, eventsList } = node;
// Do the move
const newEvent = event.clone();
eventsList.removeEvent(event);
targetEventsList.insertEvent(newEvent, targetPosition);
newEvent.delete();
this.setState(toTreeData(this.props.events), () => {
this._list.wrappedInstance.recomputeRowHeights();
});
};
render() {
return (
<div style={{ height: 400 }}>
<SortableTree
treeData={this.state.treeData}
scaffoldBlockPxWidth={22}
onChange={() => {}}
onMoveNode={this._onMoveNode}
rowHeight={({index}) => {
const extraBorderMargin = 30;
console.log(eventHeights);
const event = this.state.flatData[index];
console.log(index, "is", event);
if (!event) return 60;
console.log(eventHeights[event.ptr]);
return (eventHeights[event.ptr] + extraBorderMargin) || 60;
}}
reactVirtualizedListProps={{
ref: list => this._list = list,
}}
/>
</div>
);
}
}

View File

@@ -73,7 +73,7 @@ export default class Instruction extends Component {
);
return React.createElement(
'span',
'div',
{
className: 'instruction ' +
(instruction.dragging ? 'dragged-instruction ' : '') +

View File

@@ -85,6 +85,45 @@ export const testLayoutInstance1 = testLayout.getInitialInstances().insertNewIni
testLayoutInstance1.setX(10);
testLayoutInstance1.setY(15);
//Create a few events
//Add a new "standard" event to the scene:
var evt = testLayout.getEvents().insertNewEvent(project, "BuiltinCommonInstructions::Standard", 0);
var evt2 = testLayout.getEvents().insertNewEvent(project, "BuiltinCommonInstructions::Standard", 1);
var standardEvt = gd.asStandardEvent(evt); //We need to "cast" the event to use its specific methods (getConditions and getActions):
const makeKeyPressedCondition = () => {
const condition = new gd.Instruction();
condition.setType("KeyPressed");
condition.setParametersCount(2);
condition.setParameter(1, "Space");
return condition;
}
const makeDeleteAction = (objectToDelete) => {
var action = new gd.Instruction(); //Add a simple action
action.setType("Delete");
action.setParametersCount(2);
action.setParameter(0, objectToDelete);
return action;
}
standardEvt.getConditions().push_back(makeKeyPressedCondition());
standardEvt.getActions().push_back(makeDeleteAction("MyCharacter"));
//Add a few sub events:
{
const subEvent = evt.getSubEvents().insertNewEvent(project, "BuiltinCommonInstructions::Standard", 0);
const subStandardEvt = gd.asStandardEvent(subEvent);
subStandardEvt.getConditions().push_back(makeKeyPressedCondition());
subStandardEvt.getActions().push_back(makeDeleteAction("MyCharacter1"));
subStandardEvt.getActions().push_back(makeDeleteAction("MyCharacter2"));
}
{
const subEvent = evt.getSubEvents().insertNewEvent(project, "BuiltinCommonInstructions::Comment", 0);
const subCommentEvt = gd.asCommentEvent(subEvent);
subCommentEvt.setComment("Kain is deified. The clans tell tales of him, few know the truth. He was mortal once, as were we all. However, his contempt for humanity drove him to create me and my brethren. I am Raziel, first born of his lieutenants. I stood with Kain and my brethren at the dawn of the empire. I have served him a millennium. Over time we became less human and more ... divine.");
}
// Global objects
const globalTextObject = new gd.TextObject('GlobalTextObject');
const globalTiledSpriteObject = new gd.TiledSpriteObject('GlobalTiledSpriteObject');

View File

@@ -23,10 +23,14 @@ import EmptyEditor from '../ObjectEditor/Editors/EmptyEditor';
import ShapePainterEditor from '../ObjectEditor/Editors/ShapePainterEditor';
import AdMobEditor from '../ObjectEditor/Editors/AdMobEditor';
import ObjectsList from '../ObjectsList';
import InstancePropertiesEditor from '../InstancesEditor/InstancePropertiesEditor';
import InstancePropertiesEditor
from '../InstancesEditor/InstancePropertiesEditor';
import SerializedObjectDisplay from './SerializedObjectDisplay';
import EventsTree from '../EventsSheet/EventsTree';
import muiDecorator from './MuiDecorator';
import paperDecorator from './PaperDecorator';
import DragDropContextProvider
from '../Utils/DragDropHelpers/DragDropContextProvider';
import {
project,
shapePainterObject,
@@ -104,16 +108,12 @@ storiesOf('LocalS3Export', module)
storiesOf('LocalMobileExport', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => (
<LocalMobileExport />
));
.add('default', () => <LocalMobileExport />);
storiesOf('LocalFolderPicker', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => (
<LocalFolderPicker floatingLabelText="Export folder" />
))
.add('default', () => <LocalFolderPicker floatingLabelText="Export folder" />)
.add('full width', () => (
<LocalFolderPicker floatingLabelText="Export folder" fullWidth />
));
@@ -135,6 +135,10 @@ storiesOf('DragHandle', module)
.addDecorator(muiDecorator)
.add('default', () => <DragHandle />);
storiesOf('EventsTree', module).add('default', () => (
<DragDropContextProvider><EventsTree events={testLayout.getEvents()} /></DragDropContextProvider>
));
storiesOf('TextEditor', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
@@ -192,16 +196,14 @@ storiesOf('AdMobEditor', module)
storiesOf('EmptyEditor', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => (
<EmptyEditor />
));
.add('default', () => <EmptyEditor />);
storiesOf('ObjectsList', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => (
<SerializedObjectDisplay object={testLayout}>
<div style={{height: 250}}>
<div style={{ height: 250 }}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}

View File

@@ -4413,6 +4413,10 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
lodash.isequal@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
lodash.keys@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -4450,7 +4454,7 @@ lodash.templatesettings@^4.0.0:
dependencies:
lodash._reinterpolate "~3.0.0"
lodash.throttle@^4.1.1:
lodash.throttle@^4.0.1, lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
@@ -5118,6 +5122,10 @@ performance-now@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -5638,6 +5646,12 @@ quote-stream@~0.0.0:
minimist "0.0.8"
through2 "~0.4.1"
raf@^3.2.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27"
dependencies:
performance-now "^2.1.0"
randomatic@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"
@@ -5708,12 +5722,26 @@ react-dev-utils@^2.0.1:
strip-ansi "3.0.1"
text-table "0.2.0"
react-display-name@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/react-display-name/-/react-display-name-0.2.0.tgz#0e1f7086e45a32d07764df35ed32ff16f1259790"
react-dnd-html5-backend@2.3.0, react-dnd-html5-backend@^2.1.2:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.3.0.tgz#a45ce593f5c6944aa01114b368117c56c954804e"
dependencies:
lodash "^4.2.0"
react-dnd-scrollzone@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-dnd-scrollzone/-/react-dnd-scrollzone-4.0.0.tgz#d707170c0cd3b7ab3d991dd6a8cc0b3712454139"
dependencies:
hoist-non-react-statics "^1.2.0"
lodash.throttle "^4.0.1"
prop-types "^15.5.9"
raf "^3.2.0"
react-display-name "^0.2.0"
react-dnd@2.3.0, react-dnd@^2.1.4:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.3.0.tgz#aede61c06b968554dcf2a2445657cdbb3100be49"
@@ -5884,6 +5912,17 @@ react-sortable-hoc@^0.6.3:
lodash "^4.12.0"
prop-types "^15.5.7"
react-sortable-tree@^0.1.21:
version "0.1.21"
resolved "https://registry.yarnpkg.com/react-sortable-tree/-/react-sortable-tree-0.1.21.tgz#26a05de2012ffa46a5a4db2b71274371257daeb8"
dependencies:
lodash.isequal "^4.4.0"
prop-types "^15.5.8"
react-dnd "^2.1.4"
react-dnd-html5-backend "^2.1.2"
react-dnd-scrollzone "^4.0.0"
react-virtualized "^9.9.0"
react-split-pane@^0.1.63:
version "0.1.63"
resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.63.tgz#fadb3960cc659911dd05ffbc88acee4be9f53583"
@@ -5929,6 +5968,16 @@ react-virtualized:
dom-helpers "^2.4.0 || ^3.0.0"
loose-envify "^1.3.0"
react-virtualized@^9.9.0:
version "9.9.0"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.9.0.tgz#799a6f23819eeb82860d59b82fad33d1d420325e"
dependencies:
babel-runtime "^6.11.6"
classnames "^2.2.3"
dom-helpers "^2.4.0 || ^3.0.0"
loose-envify "^1.3.0"
prop-types "^15.5.4"
react@15.4.2:
version "15.4.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"