Refactor drag'n'drop in SortableVirtualizedItemListi and objects drag to scene

* Use a drop indicator and remove react-sortable-hoc
* Drag to scene can now follow the cursor with a temporary instance, to get an immediate feedback/preview of the object

TODO: Refactor the instance creation to use TemporaryInstances
This commit is contained in:
Florian Rival
2019-09-29 23:18:10 +01:00
committed by Florian Rival
parent 07fd7f77bd
commit 337a36ee90
24 changed files with 601 additions and 495 deletions

View File

@@ -93,6 +93,9 @@ class GD_CORE_API EventsFunctionsContainer
void MoveEventsFunction(std::size_t oldIndex, std::size_t newIndex) {
return Move(oldIndex, newIndex);
};
std::size_t GetEventsFunctionPosition(const gd::EventsFunction& eventsFunction) {
return GetPosition(eventsFunction);
};
/**
* \brief Provide a raw access to the vector containing the functions.
@@ -141,4 +144,4 @@ class GD_CORE_API EventsFunctionsContainer
} // namespace gd
#endif // GDCORE_EVENTSFUNCTIONSCONTAINER_H
#endif
#endif

View File

@@ -425,7 +425,7 @@ class GD_CORE_API ResourcesManager {
bool MoveResourceDownInList(const gd::String& name);
/**
* Change the position of the specified resource.
* \brief Change the position of the specified resource.
*/
void MoveResource(std::size_t oldIndex, std::size_t newIndex);

View File

@@ -129,6 +129,11 @@ class SerializableWithNameList {
*/
bool Has(const gd::String& name) const;
/**
* \brief Get the position of an element in the list
*/
std::size_t GetPosition(const T& element) const;
/** \name std::vector-like API
* These functions ensure that the class can be used just like a std::vector
* for iterations.

View File

@@ -109,6 +109,15 @@ void SerializableWithNameList<T>::Move(std::size_t oldIndex,
elements.insert(elements.begin() + newIndex, std::move(object));
}
template <typename T>
std::size_t SerializableWithNameList<T>::GetPosition(const T& element) const {
for(std::size_t index = 0;index<elements.size();++index) {
if (&element == elements[index].get()) return index;
}
return (size_t)-1;
}
template <typename T>
void SerializableWithNameList<T>::SerializeElementsTo(
const gd::String& elementName, SerializerElement& serializerElement) const {

View File

@@ -1683,6 +1683,7 @@ interface EventsFunctionsContainer {
void RemoveEventsFunction([Const] DOMString name);
void MoveEventsFunction(unsigned long oldIndex, unsigned long newIndex);
unsigned long GetEventsFunctionsCount();
unsigned long GetEventsFunctionPosition([Const, Ref] EventsFunction eventsFunction);
};
interface EventsBasedBehavior {
@@ -1717,6 +1718,7 @@ interface EventsBasedBehaviorsList {
void Remove([Const] DOMString name);
void Move(unsigned long oldIndex, unsigned long newIndex);
unsigned long GetCount();
unsigned long GetPosition([Const, Ref] EventsBasedBehavior item);
unsigned long size();
[Ref] EventsBasedBehavior at(unsigned long index);
@@ -1772,6 +1774,7 @@ interface EventsFunctionsExtension {
void RemoveEventsFunction([Const] DOMString name);
void MoveEventsFunction(unsigned long oldIndex, unsigned long newIndex);
unsigned long GetEventsFunctionsCount();
unsigned long GetEventsFunctionPosition([Const, Ref] EventsFunction eventsFunction);
};
interface AbstractFileSystem {

View File

@@ -73,7 +73,7 @@ export default class EventsBasedBehaviorsList extends React.Component<
) => cb(true),
};
sortableList: any;
sortableList: ?SortableVirtualizedItemList<gdEventsFunction>;
state: State = {
renamedEventsBasedBehavior: null,
searchText: '',
@@ -106,7 +106,9 @@ export default class EventsBasedBehaviorsList extends React.Component<
{
renamedEventsBasedBehavior,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -135,16 +137,26 @@ export default class EventsBasedBehaviorsList extends React.Component<
);
};
_move = (oldIndex: number, newIndex: number) => {
const { eventsBasedBehaviorsList } = this.props;
eventsBasedBehaviorsList.move(oldIndex, newIndex);
_moveSelectionTo = (
destinationEventsBasedBehavior: gdEventsBasedBehavior
) => {
const {
eventsBasedBehaviorsList,
selectedEventsBasedBehavior,
} = this.props;
if (!selectedEventsBasedBehavior) return;
eventsBasedBehaviorsList.move(
eventsBasedBehaviorsList.getPosition(selectedEventsBasedBehavior),
eventsBasedBehaviorsList.getPosition(destinationEventsBasedBehavior)
);
this.forceUpdateList();
};
forceUpdateList = () => {
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
if (this.sortableList) this.sortableList.forceUpdateGrid();
};
_copyEventsBasedBehavior = (eventsBasedBehavior: gdEventsBasedBehavior) => {
@@ -278,16 +290,17 @@ export default class EventsBasedBehaviorsList extends React.Component<
onAddNewItem={this._addNewEventsBasedBehavior}
addNewItemLabel={<Trans>Add a new behavior</Trans>}
getItemName={getEventsBasedBehaviorName}
selectedItem={selectedEventsBasedBehavior}
selectedItems={
selectedEventsBasedBehavior
? [selectedEventsBasedBehavior]
: []
}
onItemSelected={onSelectEventsBasedBehavior}
renamedItem={this.state.renamedEventsBasedBehavior}
onRename={this._rename}
onSortEnd={({ oldIndex, newIndex }) =>
this._move(oldIndex, newIndex)
}
onMoveSelectionToItem={this._moveSelectionTo}
buildMenuTemplate={this._renderEventsBasedBehaviorMenuTemplate}
helperClass="sortable-helper"
distance={20}
reactDndType="GD_EVENTS_BASED_BEHAVIOR"
/>
)}
</AutoSizer>

View File

@@ -100,7 +100,9 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
{
renamedEventsFunction: eventsFunction,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -124,16 +126,25 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
});
};
_move = (oldIndex: number, newIndex: number) => {
const { eventsFunctionsContainer } = this.props;
eventsFunctionsContainer.moveEventsFunction(oldIndex, newIndex);
_moveSelectionTo = (destinationEventsFunction: gdEventsFunction) => {
const { eventsFunctionsContainer, selectedEventsFunction } = this.props;
if (!selectedEventsFunction) return;
eventsFunctionsContainer.moveEventsFunction(
eventsFunctionsContainer.getEventsFunctionPosition(
selectedEventsFunction
),
eventsFunctionsContainer.getEventsFunctionPosition(
destinationEventsFunction
)
);
this.forceUpdateList();
};
forceUpdateList = () => {
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
if (this.sortableList) this.sortableList.forceUpdateGrid();
};
_copyEventsFunction = (eventsFunction: gdEventsFunction) => {
@@ -271,16 +282,13 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
onAddNewItem={this._addNewEventsFunction}
addNewItemLabel={<Trans>Add a new function</Trans>}
getItemName={getEventsFunctionName}
selectedItem={selectedEventsFunction}
selectedItems={selectedEventsFunction ? [selectedEventsFunction] : []}
onItemSelected={onSelectEventsFunction}
renamedItem={this.state.renamedEventsFunction}
onRename={this._rename}
onSortEnd={({ oldIndex, newIndex }) =>
this._move(oldIndex, newIndex)
}
onMoveSelectionToItem={this._moveSelectionTo}
buildMenuTemplate={this._renderEventsFunctionMenuTemplate}
helperClass="sortable-helper"
distance={20}
reactDndType="GD_EVENTS_FUNCTION"
/>
)}
</AutoSizer>

View File

@@ -2,6 +2,9 @@
import * as React from 'react';
import { dropIndicator, cantDropIndicator } from './ClassNames';
/**
* A Drop indicator line for the events sheet
*/
export default function DropIndicator({ canDrop }: {| canDrop: boolean |}) {
return <div className={canDrop ? dropIndicator : cantDropIndicator} />;
}

View File

@@ -1,17 +0,0 @@
export default class DropHandler {
constructor({ canvas, onDrop }) {
canvas.ondragover = canvas.ondrop = ev => {
ev.dataTransfer.dropEffect = 'copy';
ev.preventDefault();
};
canvas.ondrop = ev => {
const canvasRect = canvas.getBoundingClientRect();
const name = ev.dataTransfer.getData('text');
if (name)
onDrop(ev.clientX - canvasRect.left, ev.clientY - canvasRect.top, name);
ev.preventDefault();
};
}
}

View File

@@ -52,7 +52,12 @@ export default class SelectionRectangle {
return this.selectionRectangleStart;
}
makeSelectionRectangle = (lastX, lastY) => {
startSelectionRectangle = (x, y) => {
this.selectionRectangleStart = { x, y };
this.selectionRectangleEnd = { x, y };
}
updateSelectionRectangle = (lastX, lastY) => {
if (!this.selectionRectangleStart)
this.selectionRectangleStart = { x: lastX, y: lastY };

View File

@@ -0,0 +1,106 @@
// @flow
import { roundPosition } from '../Utils/GridHelpers';
const gd = global.gd;
type Props = {|
instances: gdInitialInstancesContainer,
toSceneCoordinates: (x: number, y: number) => [number, number],
options: Object,
|};
export default class TemporaryInstances {
_instances: gdInitialInstancesContainer;
_toSceneCoordinates: (x: number, y: number) => [number, number];
_temporaryInstances: Array<gdInitialInstance>;
_options: Object;
_zOrderFinder = new gd.HighestZOrderFinder();
constructor({ instances, toSceneCoordinates, options }: Props) {
this._instances = instances;
this._toSceneCoordinates = toSceneCoordinates;
this._options = options;
this._temporaryInstances = [];
}
setOptions(options: Object) {
this._options = options;
}
createOrUpdateFromObjectNames = (
x: number,
y: number,
objectNames: Array<string>
) => {
if (!objectNames.length) return;
if (!this._temporaryInstances.length) {
this._createFromObjectNames(x, y, objectNames);
} else {
this.updatePositions(x, y);
}
};
_createFromObjectNames = (
x: number,
y: number,
objectNames: Array<string>
) => {
this.deleteTemporaryInstances();
this._instances.iterateOverInstances(this._zOrderFinder);
const zOrder = this._zOrderFinder.getHighestZOrder() + 1;
const newPos = this._toSceneCoordinates(x, y);
this._temporaryInstances = objectNames.map(objectName => {
const instance: gdInitialInstance = this._instances.insertNewInitialInstance();
instance.setObjectName(objectName);
instance.setX(newPos[0]);
instance.setY(newPos[1]);
instance.setZOrder(zOrder);
// TODO: Layer here
return instance;
});
};
updatePositions = (x: number, y: number) => {
const newPos = this._toSceneCoordinates(x, y);
if (this._options.grid && this._options.snap) {
newPos[0] = roundPosition(
newPos[0],
this._options.gridWidth,
this._options.gridOffsetX
);
newPos[1] = roundPosition(
newPos[1],
this._options.gridHeight,
this._options.gridOffsetY
);
}
this._temporaryInstances.forEach(instance => {
instance.setX(newPos[0]);
instance.setY(newPos[1]);
});
};
deleteTemporaryInstances() {
this._temporaryInstances.forEach(instance => {
this._instances.removeInstance(instance);
});
this._temporaryInstances = [];
}
commitTemporaryInstances() {
this._temporaryInstances = [];
}
unmount() {
this._zOrderFinder.delete();
// Nothing to do for temporaries instances, that should have been deleted/commited by this moment.
// Don't take the risk to delete them now as this._instances might have been deleted/invalidated
// already.
}
}

View File

@@ -1,7 +1,6 @@
import React, { Component } from 'react';
import gesture from 'pixi-simple-gesture';
import KeyboardShortcuts from '../UI/KeyboardShortcuts';
import SimpleDropTarget from '../UI/DragAndDrop/SimpleDropTarget';
import InstancesRenderer from './InstancesRenderer';
import ViewPosition from './ViewPosition';
import SelectedInstances from './SelectedInstances';
@@ -13,30 +12,29 @@ import InstancesMover from './InstancesMover';
import Grid from './Grid';
import WindowBorder from './WindowBorder';
import WindowMask from './WindowMask';
import DropHandler from './DropHandler';
import BackgroundColor from './BackgroundColor';
import * as PIXI from 'pixi.js';
import FpsLimiter from './FpsLimiter';
import { startPIXITicker, stopPIXITicker } from '../Utils/PIXITicker';
import StatusBar from './StatusBar';
import CanvasCursor from './CanvasCursor';
import TemporaryInstances from './TemporaryInstances';
import { makeDropTarget } from '../UI/DragAndDrop/DropTarget';
import { objectWithContextReactDndType } from '../ObjectsList';
const styles = {
canvasArea: { flex: 1, position: 'absolute', overflow: 'hidden' },
dropCursor: { cursor: 'copy' },
};
const DropTarget = makeDropTarget(objectWithContextReactDndType);
export default class InstancesEditorContainer extends Component {
constructor() {
super();
this.lastContextMenuX = 0;
this.lastContextMenuY = 0;
this.lastCursorX = 0;
this.lastCursorY = 0;
this.fpsLimiter = new FpsLimiter(28);
}
lastContextMenuX = 0;
lastContextMenuY = 0;
lastCursorX = 0;
lastCursorY = 0;
fpsLimiter = new FpsLimiter(28);
componentDidMount() {
// Initialize the PIXI renderer, if possible
@@ -76,15 +74,6 @@ export default class InstancesEditorContainer extends Component {
return false;
});
this.pixiRenderer.view.addEventListener('pointerup', event => {
this.props.onPointerUp();
});
this.pixiRenderer.view.addEventListener('pointerover', event => {
this.props.onPointerOver();
});
this.pixiRenderer.view.addEventListener('pointerout', event => {
this.props.onPointerOut();
});
this.pixiRenderer.view.onmousewheel = event => {
if (this.keyboardShortcuts.shouldZoom()) {
this.zoomBy(event.wheelDelta / 5000);
@@ -170,16 +159,17 @@ export default class InstancesEditorContainer extends Component {
onZoomIn: this.props.onZoomIn,
});
this.dropHandler = new DropHandler({
canvas: this.canvasArea,
onDrop: this._onDrop,
});
this.canvasCursor = new CanvasCursor({
canvas: this.canvasArea,
shouldMoveView: () => this.keyboardShortcuts.shouldMoveView(),
});
this.temporaryInstances = new TemporaryInstances({
instances: this.props.initialInstances,
toSceneCoordinates: this.viewPosition.toSceneCoordinates,
options: this.props.options,
});
this._mountEditorComponents(this.props);
this._renderScene();
}
@@ -300,6 +290,7 @@ export default class InstancesEditorContainer extends Component {
this.keyboardShortcuts.unmount();
this.selectionRectangle.delete();
this.instancesRenderer.delete();
this.temporaryInstances.unmount();
if (this.nextFrame) cancelAnimationFrame(this.nextFrame);
stopPIXITicker();
}
@@ -330,6 +321,7 @@ export default class InstancesEditorContainer extends Component {
this.instancesResizer.setOptions(nextProps.options);
this.windowMask.setOptions(nextProps.options);
this.viewPosition.setOptions(nextProps.options);
this.temporaryInstances.setOptions(nextProps.options);
}
if (
@@ -385,6 +377,13 @@ export default class InstancesEditorContainer extends Component {
this.lastCursorY = y;
this.pixiRenderer.view.focus();
// Selection rectangle is only drawn in _onPanMove,
// which can happen a few milliseconds after a background
// click/touch - enough to have the selection rectangle being
// offset from the first click - which looks laggy. Set
// the start position now.
this.selectionRectangle.startSelectionRectangle(x, y);
if (
!this.keyboardShortcuts.shouldMultiSelect() &&
!this.keyboardShortcuts.shouldMoveView()
@@ -405,7 +404,7 @@ export default class InstancesEditorContainer extends Component {
this.props.onViewPositionChanged(this.viewPosition);
}
} else {
this.selectionRectangle.makeSelectionRectangle(x, y);
this.selectionRectangle.updateSelectionRectangle(x, y);
}
};
@@ -554,13 +553,6 @@ export default class InstancesEditorContainer extends Component {
this.props.onInstancesRotated(selectedInstances);
};
_onDrop = (x, y, objectName) => {
const newPos = this.viewPosition.toSceneCoordinates(x, y);
if (this.props.onAddInstance) {
this.props.onAddInstance(newPos[0], newPos[1], objectName);
}
};
clearHighlightedInstance = () => {
this.highlightedInstance.setInstance(null);
};
@@ -652,15 +644,59 @@ export default class InstancesEditorContainer extends Component {
if (!this.props.project) return null;
return (
<SimpleDropTarget>
<div
ref={canvasArea => (this.canvasArea = canvasArea)}
style={{
...styles.canvasArea,
...(this.props.showDropCursor ? styles.dropCursor : undefined),
}}
/>
</SimpleDropTarget>
<DropTarget
canDrop={() => true}
hover={monitor => {
const { temporaryInstances, canvasArea } = this;
if (!temporaryInstances || !canvasArea) return;
const { x, y } = monitor.getClientOffset();
const canvasRect = canvasArea.getBoundingClientRect();
temporaryInstances.createOrUpdateFromObjectNames(
x - canvasRect.left,
y - canvasRect.top,
this.props.selectedObjectNames
);
}}
drop={monitor => {
const { temporaryInstances, canvasArea } = this;
if (!temporaryInstances || !canvasArea) return;
if (monitor.didDrop()) {
// Drop was done somewhere else (in a child of the canvas:
// should not happen, but still handling this case).
temporaryInstances.deleteTemporaryInstances();
return;
}
const { x, y } = monitor.getClientOffset();
const canvasRect = canvasArea.getBoundingClientRect();
temporaryInstances.updatePositions(
x - canvasRect.left,
y - canvasRect.top
);
temporaryInstances.commitTemporaryInstances();
}}
>
{({ connectDropTarget, isOver }) => {
// The children are re-rendered when isOver change:
// take this opportunity to delete any temporary instances
// if the dragging is not done anymore over the canvas.
if (this.temporaryInstances && !isOver) {
this.temporaryInstances.deleteTemporaryInstances();
}
return connectDropTarget(
<div
ref={canvasArea => (this.canvasArea = canvasArea)}
style={{
...styles.canvasArea,
...(this.props.showDropCursor ? styles.dropCursor : undefined),
}}
/>
);
}}
</DropTarget>
);
}
}

View File

@@ -227,7 +227,7 @@ export default class GroupsListContainer extends React.Component<Props, State> {
{
renamedGroupWithScope: groupWithContext,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {if (this.sortableList) this.sortableList.forceUpdateGrid();}
);
};
@@ -285,7 +285,7 @@ export default class GroupsListContainer extends React.Component<Props, State> {
}
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
if (this.sortableList) this.sortableList.forceUpdateGrid();
};
_setAsGlobalGroup = (groupWithContext: GroupWithContext) => {

View File

@@ -41,6 +41,8 @@ const styles = {
},
};
export const objectWithContextReactDndType = 'GD_OBJECT_WITH_CONTEXT';
const getObjectWithContextName = (objectWithContext: ObjectWithContext) =>
objectWithContext.object.getName();
@@ -93,16 +95,11 @@ type Props = {|
onObjectPasted?: gdObject => void,
canRenameObject: (newName: string) => boolean,
onStartDraggingObject: gdObject => void,
onEndDraggingObject: () => void,
getThumbnail: (project: gdProject, object: Object) => string,
canMoveObjects: boolean,
|};
export default class ObjectsList extends React.Component<Props, State> {
sortableList: any;
sortableList: ?SortableVirtualizedItemList<ObjectWithContext>;
_displayedObjectWithContextsList: ObjectWithContextList = [];
state = {
newObjectDialogOpen: false,
@@ -284,7 +281,9 @@ export default class ObjectsList extends React.Component<Props, State> {
{
renamedObjectWithContext: objectWithContext,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -311,47 +310,46 @@ export default class ObjectsList extends React.Component<Props, State> {
}
};
_move = (oldIndex: number, newIndex: number) => {
// Moving objects can be discarded by the parent (this is used to allow
// dropping objects on the scene editor).
if (!this.props.canMoveObjects) return;
const { project, objectsContainer } = this.props;
const movedObjectWithContext = this._displayedObjectWithContextsList[
oldIndex
];
const destinationObjectWithContext = this._displayedObjectWithContextsList[
newIndex
];
if (!movedObjectWithContext || !destinationObjectWithContext) return;
if (movedObjectWithContext.global !== destinationObjectWithContext.global) {
// Can't move an object from the objects container to the global objects
// or vice-versa.
return;
}
const container: gdObjectsContainer = movedObjectWithContext.global
? project
: objectsContainer;
container.moveObject(
container.getObjectPosition(movedObjectWithContext.object.getName()),
container.getObjectPosition(destinationObjectWithContext.object.getName())
_canMoveSelectionTo = (destinationObjectWithContext: ObjectWithContext) => {
// Check if at least one element in the selection can be moved.
const selectedObjects = this._displayedObjectWithContextsList.filter(
objectWithContext =>
this.props.selectedObjectNames.indexOf(
objectWithContext.object.getName()
) !== -1
);
this.forceUpdateList();
return selectedObjects.filter(movedObjectWithContext => {
return movedObjectWithContext.global === destinationObjectWithContext.global;
}).length > 0;
};
_onStartDraggingObject = ({ index }: { index: number }) => {
const draggedObjectWithContext = this._displayedObjectWithContextsList[
index
];
if (!draggedObjectWithContext) {
return;
}
_moveSelectionTo = (destinationObjectWithContext: ObjectWithContext) => {
const { project, objectsContainer } = this.props;
this.props.onStartDraggingObject(draggedObjectWithContext.object);
const container: gdObjectsContainer = destinationObjectWithContext.global
? project
: objectsContainer;
const selectedObjects = this._displayedObjectWithContextsList.filter(
objectWithContext =>
this.props.selectedObjectNames.indexOf(
objectWithContext.object.getName()
) !== -1
);
selectedObjects.forEach(movedObjectWithContext => {
if (movedObjectWithContext.global !== destinationObjectWithContext.global) {
// Can't move an object from the objects container to the global objects
// or vice-versa.
return;
}
container.moveObject(
container.getObjectPosition(movedObjectWithContext.object.getName()),
container.getObjectPosition(destinationObjectWithContext.object.getName())
);
});
this.forceUpdateList();
};
_setAsGlobalObject = (objectWithContext: ObjectWithContext) => {
@@ -388,7 +386,7 @@ export default class ObjectsList extends React.Component<Props, State> {
forceUpdateList = () => {
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
if (this.sortableList) this.sortableList.forceUpdateGrid();
};
_openEditTagDialog = (tagEditedObject: ?gdObject) => {
@@ -405,7 +403,7 @@ export default class ObjectsList extends React.Component<Props, State> {
this.forceUpdateList();
};
_selectObject = (objectWithContext: ObjectWithContext) => {
_selectObject = (objectWithContext: ?ObjectWithContext) => {
this.props.onObjectSelected(
objectWithContext ? objectWithContext.object.getName() : ''
);
@@ -535,13 +533,9 @@ export default class ObjectsList extends React.Component<Props, State> {
renamedItem={renamedObjectWithContext}
onRename={this._rename}
buildMenuTemplate={this._renderObjectMenuTemplate}
onSortStart={this._onStartDraggingObject}
onSortEnd={({ oldIndex, newIndex }) => {
this.props.onEndDraggingObject();
this._move(oldIndex, newIndex);
}}
helperClass="sortable-helper"
distance={20}
onMoveSelectionToItem={this._moveSelectionTo}
canMoveSelectionToItem={this._canMoveSelectionTo}
reactDndType={objectWithContextReactDndType}
/>
)}
</AutoSizer>

View File

@@ -39,7 +39,7 @@ type State = {|
type Props = {|
project: gdProject,
selectedResource: ?gdResource,
onSelectResource: (resource: gdResource) => void,
onSelectResource: (resource: ?gdResource) => void,
onDeleteResource: (resource: gdResource) => void,
onRenameResource: (
resource: gdResource,
@@ -166,7 +166,9 @@ export default class ResourcesList extends React.Component<Props, State> {
{
renamedResource: resource,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -196,16 +198,21 @@ export default class ResourcesList extends React.Component<Props, State> {
});
};
_move = (oldIndex: number, newIndex: number) => {
const { project } = this.props;
_moveSelectionTo = (destinationResource: gdResource) => {
const { project, selectedResource } = this.props;
if (!selectedResource) return;
project.getResourcesManager().moveResource(oldIndex, newIndex);
const resourcesManager = project.getResourcesManager();
resourcesManager.moveResource(
resourcesManager.getResourcePosition(selectedResource.getName()),
resourcesManager.getResourcePosition(destinationResource.getName())
);
this.forceUpdateList();
};
forceUpdateList = () => {
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
if (this.sortableList) this.sortableList.forceUpdateGrid();
};
_renderResourceMenuTemplate = (resource: gdResource, _index: number) => {
@@ -351,17 +358,14 @@ export default class ResourcesList extends React.Component<Props, State> {
width={width}
height={height}
getItemName={getResourceName}
selectedItem={selectedResource}
selectedItems={selectedResource ? [selectedResource] : []}
onItemSelected={onSelectResource}
renamedItem={this.state.renamedResource}
onRename={this._rename}
onSortEnd={({ oldIndex, newIndex }) =>
this._move(oldIndex, newIndex)
}
onMoveSelectionToItem={this._moveSelectionTo}
buildMenuTemplate={this._renderResourceMenuTemplate}
helperClass="sortable-helper"
distance={20}
erroredItems={this.state.resourcesWithErrors}
reactDndType="GD_RESOURCE"
/>
)}
</AutoSizer>

View File

@@ -132,10 +132,6 @@ type State = {|
editedGroup: ?gdObjectGroup,
// State for "drag'n'dropping" from the objects list to the instances editor:
objectDraggedFromList: ?gdObject,
canDropDraggedObject: boolean,
uiSettings: Object,
history: HistoryState,
@@ -183,10 +179,6 @@ export default class SceneEditor extends React.Component<Props, State> {
newObjectInstancePosition: null,
editedGroup: null,
// State for "drag'n'dropping" from the objects list to the instances editor:
objectDraggedFromList: null,
canDropDraggedObject: false,
uiSettings: props.initialUiSettings,
history: getHistoryInitialState(props.initialInstances, {
historyMaxSize: 50,
@@ -362,77 +354,6 @@ export default class SceneEditor extends React.Component<Props, State> {
this.editObject(project.getObject(objectName));
};
/**
* Called when an object is started to be dragged from the object list.
* See `_onPointerOverInstancesEditor`, `_onPointerOutInstancesEditor` and
* `_onPointerUpInstancesEditor` for the drag'n'drop workflow.
*/
_onStartDraggingObjectFromList = (object: gdObject) => {
// "Hijack" the name of the object that is dragged in the objects list.
// We'll then listen to "pointer over" events to see if the object
// is the dragged on the instances editor.
this.setState({
objectDraggedFromList: object,
});
};
_onEndDraggingObjectFromList = () => {
// If the dragged object is not being dropped on the instances editor,
// clear the dragged object so that we don't keep it the state (otherwise
// we could think later that a dragging is still occuring when cursor
// is over the instances editor).
if (!this.state.canDropDraggedObject) {
this.setState({
objectDraggedFromList: null,
});
}
};
_onPointerOverInstancesEditor = () => {
// If an object is dragged, and cursor is over the instances editor,
// mark in the state that we can drop an instance of this object.
if (this.state.objectDraggedFromList && !this.state.canDropDraggedObject) {
this.setState({
canDropDraggedObject: true,
});
}
};
_onPointerOutInstancesEditor = () => {
// If cursor is going out of the instances editor,
// mark in the state that we cannot drop an instance anymore.
if (this.state.canDropDraggedObject) {
this.setState({
canDropDraggedObject: false,
});
}
};
_onPointerUpInstancesEditor = () => {
if (this.state.canDropDraggedObject) {
if (this.editor) {
const cursorPosition = this.editor.getLastCursorPosition();
if (this.state.objectDraggedFromList)
this._addInstance(
cursorPosition[0],
cursorPosition[1],
this.state.objectDraggedFromList.getName()
);
}
// Wait 30ms after dropping the object before reseting the canDropDraggedObject state boolean
// to ensure ObjectsList will be prevented to actually move object in the list.
setTimeout(
() =>
this.setState({
canDropDraggedObject: false,
objectDraggedFromList: null,
}),
30 // This value is very conservative, and timeout may not be needed at all
);
}
};
editGroup = (group: ?gdObjectGroup) => {
this.setState({ editedGroup: group });
};
@@ -518,6 +439,8 @@ export default class SceneEditor extends React.Component<Props, State> {
_addInstance = (x: number, y: number, objectName: string) => {
if (!objectName) return;
// TODO: Replace this by _onInstancesAdded (like _onInstancesMoved)
const instance = this.props.initialInstances.insertNewInitialInstance();
instance.setObjectName(objectName);
if (this.state.uiSettings.grid) {
@@ -607,6 +530,8 @@ export default class SceneEditor extends React.Component<Props, State> {
* previously chosen (see `newObjectInstancePosition`).
*/
_addNewObjectInstance = (newObjectName: string) => {
// TODO: Move _addNewObjectInstance, _addInstance, _onAddInstanceUnderCursor to use
// InstancesEditor.
const { newObjectInstancePosition } = this.state;
if (!newObjectInstancePosition) {
return;
@@ -1010,9 +935,7 @@ export default class SceneEditor extends React.Component<Props, State> {
onInstancesMoved={this._onInstancesMoved}
onInstancesResized={this._onInstancesResized}
onInstancesRotated={this._onInstancesRotated}
onPointerUp={this._onPointerUpInstancesEditor}
onPointerOver={this._onPointerOverInstancesEditor}
onPointerOut={this._onPointerOutInstancesEditor}
selectedObjectNames={this.state.selectedObjectNames}
onContextMenu={this._onContextMenu}
onCopy={() => this.copySelection({ useLastCursorPosition: true })}
onCut={() => this.cutSelection({ useLastCursorPosition: true })}
@@ -1021,7 +944,6 @@ export default class SceneEditor extends React.Component<Props, State> {
onRedo={this.redo}
onZoomOut={this.zoomOut}
onZoomIn={this.zoomIn}
showDropCursor={this.state.canDropDraggedObject}
wrappedEditorRef={editor => (this.editor = editor)}
pauseRendering={!isActive}
/>
@@ -1056,9 +978,6 @@ export default class SceneEditor extends React.Component<Props, State> {
onObjectSelected={this._onObjectSelected}
onRenameObject={this._onRenameObject}
onObjectPasted={() => this.updateBehaviorsSharedData()}
onStartDraggingObject={this._onStartDraggingObjectFromList}
onEndDraggingObject={this._onEndDraggingObjectFromList}
canMoveObjects={!this.state.canDropDraggedObject}
selectedObjectTags={this.state.selectedObjectTags}
onChangeSelectedObjectTags={selectedObjectTags =>
this.setState({

View File

@@ -1,5 +1,6 @@
import { Component } from 'react';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import { DragDropContext } from 'react-dnd';
class DragAndDropContextProvider extends Component {

View File

@@ -14,7 +14,8 @@ type Props<DraggedItemType> = {|
canDrop: boolean,
}) => React.Node,
canDrop: (item: DraggedItemType) => boolean,
drop: () => void,
hover?: (monitor: DropTargetMonitor) => void,
drop: (monitor: DropTargetMonitor) => void,
|};
type DropTargetProps = {|
@@ -26,16 +27,19 @@ type DropTargetProps = {|
export const makeDropTarget = <DraggedItemType>(
reactDndType: string
): ((Props<DraggedItemType>) => React.Node) => {
const instructionTarget = {
const targetSpec = {
canDrop(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
const item = monitor.getItem();
return item && props.canDrop(item);
},
hover(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
if (props.hover) props.hover(monitor);
},
drop(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
if (monitor.didDrop()) {
return; // Drop already handled by another target
}
props.drop();
props.drop(monitor);
},
};
@@ -50,17 +54,15 @@ export const makeDropTarget = <DraggedItemType>(
};
}
const InnerDropTarget = DropTarget(
reactDndType,
instructionTarget,
targetCollect
)(({ children, connectDropTarget, isOver, canDrop }) => {
return children({
connectDropTarget,
isOver,
canDrop,
});
});
const InnerDropTarget = DropTarget(reactDndType, targetSpec, targetCollect)(
({ children, connectDropTarget, isOver, canDrop }) => {
return children({
connectDropTarget,
isOver,
canDrop,
});
}
);
return (props: Props<DraggedItemType>) => <InnerDropTarget {...props} />;
};

View File

@@ -1,37 +0,0 @@
import { Component } from 'react';
import { NativeTypes } from 'react-dnd-html5-backend';
import { DropTarget } from 'react-dnd';
class SimpleDropTarget extends Component {
render() {
const { connectDropTarget } = this.props;
return connectDropTarget(this.props.children);
}
}
function collect(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
// You can ask the monitor about the current drag state:
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType(),
};
}
const spec = {
drop(props, monitor, component) {
if (monitor.didDrop()) {
// If you want, you can check whether some nested
// target already handled drop
return;
}
const item = monitor.getItem();
return { item };
},
};
export default DropTarget(NativeTypes.TEXT, spec, collect)(SimpleDropTarget);

View File

@@ -69,6 +69,7 @@ type ListItemProps = {|
|},
style?: {|
color?: string,
backgroundColor?: string,
borderBottom?: string,
opacity?: number,

View File

@@ -0,0 +1,31 @@
// @flow
import * as React from 'react';
import ThemeConsumer from '../Theme/ThemeConsumer';
const styles = {
dropIndicator: {
borderTop: '2px solid #18dcf2',
height: 0,
marginTop: '-1px',
marginBottom: '-1px',
width: '100%',
boxSizing: 'border-box',
},
};
export default function DropIndicator({ canDrop }: {| canDrop: boolean |}) {
return (
<ThemeConsumer>
{gdevelopTheme => (
<div
style={{
...styles.dropIndicator,
borderColor: canDrop
? gdevelopTheme.listItem.selectedBackgroundColor
: gdevelopTheme.listItem.selectedErrorBackgroundColor,
}}
/>
)}
</ThemeConsumer>
);
}

View File

@@ -3,7 +3,6 @@ import React from 'react';
import { ListItem } from '../List';
import ListIcon from '../ListIcon';
import TextField, { noMarginTextFieldInListItemTopOffset } from '../TextField';
import { type Item } from '.';
import ThemeConsumer from '../Theme/ThemeConsumer';
const styles = {
@@ -19,26 +18,24 @@ const styles = {
const LEFT_MOUSE_BUTTON = 0;
type Props = {
index: number,
type Props<Item> = {
item: Item,
itemName: string,
isBold: boolean,
onRename: string => void,
editingName: boolean,
getThumbnail?: () => string,
selected: true,
selected: boolean,
onItemSelected: (?Item) => void,
errorStatus: '' | 'error' | 'warning',
buildMenuTemplate: () => Array<any>,
onEdit: Item => void,
style: Object,
onEdit?: ?(Item) => void,
};
class ItemRow extends React.Component<Props> {
class ItemRow<Item> extends React.Component<Props<Item>> {
textField: ?TextField;
componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps: Props<Item>) {
if (!prevProps.editingName && this.props.editingName) {
setTimeout(() => {
if (this.textField) this.textField.focus();
@@ -52,9 +49,10 @@ class ItemRow extends React.Component<Props> {
itemName,
isBold,
selected,
style,
getThumbnail,
errorStatus,
onEdit,
onItemSelected,
} = this.props;
return (
@@ -110,7 +108,7 @@ class ItemRow extends React.Component<Props> {
return (
<ListItem
style={{ ...itemStyle, ...style }}
style={{ ...itemStyle }}
primaryText={label}
leftIcon={
getThumbnail && <ListIcon iconSize={32} src={getThumbnail()} />
@@ -118,18 +116,18 @@ class ItemRow extends React.Component<Props> {
displayMenuButton
buildMenuTemplate={this.props.buildMenuTemplate}
onClick={() => {
if (!this.props.onItemSelected) return;
if (!onItemSelected) return;
if (this.props.editingName) return;
this.props.onItemSelected(selected ? null : item);
onItemSelected(selected ? null : item);
}}
onDoubleClick={event => {
if (event.button !== LEFT_MOUSE_BUTTON) return;
if (!this.props.onEdit) return;
if (!onEdit) return;
if (this.props.editingName) return;
this.props.onItemSelected(null);
this.props.onEdit(item);
onItemSelected(null);
onEdit(item);
}}
/>
);

View File

@@ -3,38 +3,17 @@ import * as React from 'react';
import { List } from 'react-virtualized';
import ItemRow from './ItemRow';
import { AddListItem } from '../ListCommonItem';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import { listItemWith32PxIconHeight, listItemWithoutIconHeight } from '../List';
import { makeDragSourceAndDropTarget } from '../DragAndDrop/DragSourceAndDropTarget';
import DropIndicator from './DropIndicator';
const SortableItemRow = SortableElement(props => {
const { style, ...otherProps } = props;
return (
<div style={style}>
<ItemRow {...otherProps} />
</div>
);
});
const SortableAddItemRow = SortableElement(props => {
const { style, ...otherProps } = props;
return (
<div style={style}>
<AddListItem {...otherProps} />
</div>
);
});
export type Item = {
key: string | number,
};
type ItemsListProps = {
type Props<Item> = {|
height: number,
width: number,
fullList: Array<Item>,
selectedItem: ?Item, // TODO
selectedItems: ?Array<Item>,
selectedItems: Array<Item>,
onAddNewItem?: () => void,
addNewItemLabel?: React.Node | string,
onRename: (Item, string) => void,
getItemName: Item => string,
getItemThumbnail?: Item => string,
@@ -42,13 +21,20 @@ type ItemsListProps = {
onItemSelected: (?Item) => void,
onEditItem?: Item => void,
renamedItem: ?Item,
addNewItemLabel: React.Node | string,
erroredItems?: { [string]: '' | 'error' | 'warning' },
buildMenuTemplate: (Item, index: number) => any,
};
onMoveSelectionToItem: (destinationItem: Item) => void,
canMoveSelectionToItem?: ?(destinationItem: Item) => boolean,
reactDndType: string,
|};
class ItemsList extends React.Component<ItemsListProps, *> {
export default class SortableVirtualizedItemList<Item> extends React.Component<
Props<Item>
> {
_list: ?List;
DragSourceAndDropTarget = makeDragSourceAndDropTarget<Item>(
this.props.reactDndType
);
forceUpdateGrid() {
if (this._list) this._list.forceUpdateGrid();
@@ -59,7 +45,6 @@ class ItemsList extends React.Component<ItemsListProps, *> {
height,
width,
fullList,
selectedItem,
selectedItems,
addNewItemLabel,
renamedItem,
@@ -69,7 +54,10 @@ class ItemsList extends React.Component<ItemsListProps, *> {
onAddNewItem,
isItemBold,
onEditItem,
onMoveSelectionToItem,
canMoveSelectionToItem,
} = this.props;
const { DragSourceAndDropTarget } = this;
return (
<List
@@ -92,14 +80,13 @@ class ItemsList extends React.Component<ItemsListProps, *> {
|}) => {
if (index >= fullList.length) {
return (
<SortableAddItemRow
index={fullList.length}
key={key}
style={style}
disabled
onClick={onAddNewItem}
primaryText={addNewItemLabel}
/>
<div style={style} key={key}>
<AddListItem
disabled
onClick={onAddNewItem}
primaryText={addNewItemLabel}
/>
</div>
);
}
@@ -108,30 +95,55 @@ class ItemsList extends React.Component<ItemsListProps, *> {
const itemName = getItemName(item);
return (
<SortableItemRow
index={index}
key={key}
item={item}
itemName={itemName}
isBold={isItemBold ? isItemBold(item) : false}
style={style}
onRename={newName => this.props.onRename(item, newName)}
editingName={nameBeingEdited}
getThumbnail={
getItemThumbnail ? () => getItemThumbnail(item) : undefined
}
selected={
selectedItems && selectedItems.indexOf(item) !== -1
? true
: item === selectedItem
}
onItemSelected={this.props.onItemSelected}
errorStatus={erroredItems ? erroredItems[itemName] || '' : ''}
buildMenuTemplate={() =>
this.props.buildMenuTemplate(item, index)
}
onEdit={onEditItem}
/>
<div style={style} key={key}>
<DragSourceAndDropTarget
beginDrag={() => {
this.props.onItemSelected(item);
return {};
}}
canDrop={() =>
canMoveSelectionToItem ? canMoveSelectionToItem(item) : true
}
drop={() => {
onMoveSelectionToItem(item);
}}
>
{({ connectDragSource, connectDropTarget, isOver, canDrop }) =>
// Add an extra div because connectDropTarget/connectDragSource can
// only be used on native elements
connectDropTarget(
connectDragSource(
<div>
{isOver && <DropIndicator canDrop={canDrop} />}
<ItemRow
item={item}
itemName={itemName}
isBold={isItemBold ? isItemBold(item) : false}
onRename={newName =>
this.props.onRename(item, newName)
}
editingName={nameBeingEdited}
getThumbnail={
getItemThumbnail
? () => getItemThumbnail(item)
: undefined
}
selected={selectedItems.indexOf(item) !== -1}
onItemSelected={this.props.onItemSelected}
errorStatus={
erroredItems ? erroredItems[itemName] || '' : ''
}
buildMenuTemplate={() =>
this.props.buildMenuTemplate(item, index)
}
onEdit={onEditItem}
/>
</div>
)
)
}
</DragSourceAndDropTarget>
</div>
);
}}
width={width}
@@ -139,6 +151,3 @@ class ItemsList extends React.Component<ItemsListProps, *> {
);
}
}
const SortableItemsList = SortableContainer(ItemsList, { withRef: true });
export default SortableItemsList;

View File

@@ -723,83 +723,93 @@ storiesOf('UI Building Blocks/ClosableTabs', module)
<ValueStateHolder
initialValue={0}
render={(value, onChange) => (
<FixedHeightFlexContainer height={400}>
<Column expand>
<ClosableTabs>
<ClosableTab
onActivated={action('Tab 1 activated')}
closable
active={value === 0}
label="Tab 1"
onClick={() => onChange(0)}
onClose={action('Close tab 1')}
onCloseAll={action('Close all')}
onCloseOthers={action('Close others')}
/>
<ClosableTab
onActivated={action('Tab 2 activated')}
closable
active={value === 1}
label="Tab 2"
onClick={() => onChange(1)}
onClose={action('Close tab 2')}
onCloseAll={action('Close all')}
onCloseOthers={action('Close others')}
/>
<ClosableTab
onActivated={action('Tab 3 activated')}
closable
active={value === 2}
label="Tab 3"
onClick={() => onChange(2)}
onClose={action('Close tab 3')}
onCloseAll={action('Close all')}
onCloseOthers={action('Close others')}
/>
</ClosableTabs>
{
<TabContentContainer active={value === 0}>
<div
style={{ backgroundColor: 'green', height: '100%', flex: 1 }}
>
The second tab has a list of objects. Check that the scrolling
position is maintained while navigating between tabs.
</div>
</TabContentContainer>
}
{
<TabContentContainer active={value === 1}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}
objectsContainer={testLayout}
onEditObject={action('On edit object')}
selectedObjectNames={[]}
selectedObjectTags={[]}
onChangeSelectedObjectTags={() => {}}
getAllObjectTags={() => []}
canMoveObjects
canRenameObject={() => true}
onDeleteObject={(objectWithContext, cb) => cb(true)}
onRenameObject={(objectWithContext, newName, cb) => cb(true)}
onStartDraggingObject={() => {}}
onEndDraggingObject={() => {}}
onObjectCreated={() => {}}
onObjectSelected={() => {}}
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={400}>
<Column expand>
<ClosableTabs>
<ClosableTab
onActivated={action('Tab 1 activated')}
closable
active={value === 0}
label="Tab 1"
onClick={() => onChange(0)}
onClose={action('Close tab 1')}
onCloseAll={action('Close all')}
onCloseOthers={action('Close others')}
/>
</TabContentContainer>
}
{
<TabContentContainer active={value === 2}>
<div
style={{ backgroundColor: 'green', height: '100%', flex: 1 }}
>
Tab 3 content
</div>
</TabContentContainer>
}
</Column>
</FixedHeightFlexContainer>
<ClosableTab
onActivated={action('Tab 2 activated')}
closable
active={value === 1}
label="Tab 2"
onClick={() => onChange(1)}
onClose={action('Close tab 2')}
onCloseAll={action('Close all')}
onCloseOthers={action('Close others')}
/>
<ClosableTab
onActivated={action('Tab 3 activated')}
closable
active={value === 2}
label="Tab 3"
onClick={() => onChange(2)}
onClose={action('Close tab 3')}
onCloseAll={action('Close all')}
onCloseOthers={action('Close others')}
/>
</ClosableTabs>
{
<TabContentContainer active={value === 0}>
<div
style={{
backgroundColor: 'green',
height: '100%',
flex: 1,
}}
>
The second tab has a list of objects. Check that the
scrolling position is maintained while navigating between
tabs.
</div>
</TabContentContainer>
}
{
<TabContentContainer active={value === 1}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}
objectsContainer={testLayout}
onEditObject={action('On edit object')}
selectedObjectNames={[]}
selectedObjectTags={[]}
onChangeSelectedObjectTags={() => {}}
getAllObjectTags={() => []}
canRenameObject={() => true}
onDeleteObject={(objectWithContext, cb) => cb(true)}
onRenameObject={(objectWithContext, newName, cb) =>
cb(true)
}
onObjectCreated={() => {}}
onObjectSelected={() => {}}
/>
</TabContentContainer>
}
{
<TabContentContainer active={value === 2}>
<div
style={{
backgroundColor: 'green',
height: '100%',
flex: 1,
}}
>
Tab 3 content
</div>
</TabContentContainer>
}
</Column>
</FixedHeightFlexContainer>
</DragAndDropContextProvider>
)}
/>
));
@@ -2030,57 +2040,57 @@ storiesOf('ObjectsList', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('default', () => (
<SerializedObjectDisplay object={testLayout}>
<div style={{ height: 250 }}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}
objectsContainer={testLayout}
onEditObject={action('On edit object')}
onObjectCreated={action('On object created')}
selectedObjectNames={[]}
selectedObjectTags={[]}
onChangeSelectedObjectTags={selectedObjectTags => {}}
getAllObjectTags={() => []}
canMoveObjects
canRenameObject={() => true}
onDeleteObject={(objectWithContext, cb) => cb(true)}
onRenameObject={(objectWithContext, newName, cb) => cb(true)}
onStartDraggingObject={() => {}}
onEndDraggingObject={() => {}}
onObjectSelected={() => {}}
/>
</div>
</SerializedObjectDisplay>
<DragAndDropContextProvider>
<SerializedObjectDisplay object={testLayout}>
<div style={{ height: 250 }}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}
objectsContainer={testLayout}
onEditObject={action('On edit object')}
onObjectCreated={action('On object created')}
selectedObjectNames={[]}
selectedObjectTags={[]}
onChangeSelectedObjectTags={selectedObjectTags => {}}
getAllObjectTags={() => []}
canRenameObject={() => true}
onDeleteObject={(objectWithContext, cb) => cb(true)}
onRenameObject={(objectWithContext, newName, cb) => cb(true)}
onObjectSelected={() => {}}
/>
</div>
</SerializedObjectDisplay>
</DragAndDropContextProvider>
))
.add('with tags', () => (
<SerializedObjectDisplay object={testLayout}>
<div style={{ height: 250 }}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}
objectsContainer={testLayout}
onEditObject={action('On edit object')}
onObjectCreated={action('On object created')}
selectedObjectNames={[]}
selectedObjectTags={['Tag1', 'Tag2']}
onChangeSelectedObjectTags={action('on change selected object tags')}
getAllObjectTags={() => [
'Tag1',
'Tag2',
'Looooooooooong Tag 3',
'Unselected Tag 4',
]}
canMoveObjects
canRenameObject={() => true}
onDeleteObject={(objectWithContext, cb) => cb(true)}
onRenameObject={(objectWithContext, newName, cb) => cb(true)}
onStartDraggingObject={() => {}}
onEndDraggingObject={() => {}}
onObjectSelected={() => {}}
/>
</div>
</SerializedObjectDisplay>
<DragAndDropContextProvider>
<SerializedObjectDisplay object={testLayout}>
<div style={{ height: 250 }}>
<ObjectsList
getThumbnail={() => 'res/unknown32.png'}
project={project}
objectsContainer={testLayout}
onEditObject={action('On edit object')}
onObjectCreated={action('On object created')}
selectedObjectNames={[]}
selectedObjectTags={['Tag1', 'Tag2']}
onChangeSelectedObjectTags={action(
'on change selected object tags'
)}
getAllObjectTags={() => [
'Tag1',
'Tag2',
'Looooooooooong Tag 3',
'Unselected Tag 4',
]}
canRenameObject={() => true}
onDeleteObject={(objectWithContext, cb) => cb(true)}
onRenameObject={(objectWithContext, newName, cb) => cb(true)}
onObjectSelected={() => {}}
/>
</div>
</SerializedObjectDisplay>
</DragAndDropContextProvider>
));
storiesOf('ObjectSelector', module)