mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
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:

committed by
Florian Rival

parent
07fd7f77bd
commit
337a36ee90
@@ -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
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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} />;
|
||||
}
|
||||
|
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
@@ -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 };
|
||||
|
||||
|
106
newIDE/app/src/InstancesEditor/TemporaryInstances.js
Normal file
106
newIDE/app/src/InstancesEditor/TemporaryInstances.js
Normal 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.
|
||||
}
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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) => {
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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({
|
||||
|
@@ -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 {
|
||||
|
@@ -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} />;
|
||||
};
|
||||
|
@@ -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);
|
@@ -69,6 +69,7 @@ type ListItemProps = {|
|
||||
|},
|
||||
|
||||
style?: {|
|
||||
color?: string,
|
||||
backgroundColor?: string,
|
||||
borderBottom?: string,
|
||||
opacity?: number,
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user