Compare commits

...

7 Commits

Author SHA1 Message Date
Florian Rival
a97948cdf3 Add logging for startup times 2019-10-01 11:34:51 +01:00
Florian Rival
0a86424f15 Rename TemporaryInstances to InstancesAdder and use it in SceneEditor
There is a unique place where the logic to create instances is now.
2019-10-01 00:14:54 +01:00
Florian Rival
95930c5bfd 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
2019-09-30 23:23:43 +01:00
Florian Rival
fac1246445 Refactor InstructionsList to use DropTarget 2019-09-29 16:50:40 +01:00
Florian Rival
9772520d24 Move Utils/DragDropHelpers into UI/DragAndDrop 2019-09-29 16:29:20 +01:00
Florian Rival
86eef2dab4 Refactor Instruction to use DragSourceAndDropTarget
* Fix error "Expected to find a valid target" when dragging instructions on top of a subinstructions list (react-dnd does not support well having a drop target/source inside another in these conditions it seems)
2019-09-29 16:14:46 +01:00
Florian Rival
809c04f88e Use SortableVirtualizedItemList for ObjectsList implementation 2019-09-29 14:21:44 +01:00
36 changed files with 1485 additions and 995 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

@@ -30,6 +30,10 @@
<!-- Root div used for React `App` component rendering-->
<div id="root"></div>
<script>
window["GD_STARTUP_TIMES"] = [["indexHtmlFirstScriptStarted", performance.now()]];
</script>
<!-- GDevelop.js core -->
<script>
//Ensure module is not defined to avoid GDevelop.js to think
@@ -40,6 +44,11 @@
</script>
<script src="%PUBLIC_URL%/libGD.js"></script>
<script>
window["GD_STARTUP_TIMES"] = window["GD_STARTUP_TIMES"] || [];
window["GD_STARTUP_TIMES"].push(["libGDLoadedTime", performance.now()]);
</script>
<!-- Monaco Editor support -->
<script>
// Monaco editor is using amd require. Save the Node.js require if it exists (Electron)
@@ -65,5 +74,10 @@
}
};
</script>
<script>
window["GD_STARTUP_TIMES"] = window["GD_STARTUP_TIMES"] || [];
window["GD_STARTUP_TIMES"].push(["monacoEditorLoaderLoaded", performance.now()]);
</script>
</body>
</html>

View File

@@ -31,6 +31,10 @@ type State = {|
searchText: string,
|};
const getEventsBasedBehaviorName = (
eventsBasedBehavior: gdEventsBasedBehavior
) => eventsBasedBehavior.getName();
type Props = {|
project: gdProject,
eventsBasedBehaviorsList: gdEventsBasedBehaviorsList,
@@ -69,7 +73,7 @@ export default class EventsBasedBehaviorsList extends React.Component<
) => cb(true),
};
sortableList: any;
sortableList: ?SortableVirtualizedItemList<gdEventsFunction>;
state: State = {
renamedEventsBasedBehavior: null,
searchText: '',
@@ -102,7 +106,9 @@ export default class EventsBasedBehaviorsList extends React.Component<
{
renamedEventsBasedBehavior,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -131,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) => {
@@ -250,15 +266,10 @@ export default class EventsBasedBehaviorsList extends React.Component<
} = this.props;
const { searchText } = this.state;
const list = [
...filterEventsBasedBehaviorsList(
enumerateEventsBasedBehaviors(eventsBasedBehaviorsList),
searchText
),
{
key: 'add-item-row',
},
];
const list = filterEventsBasedBehaviorsList(
enumerateEventsBasedBehaviors(eventsBasedBehaviorsList),
searchText
);
// Force List component to be mounted again if project or eventsBasedBehaviorsList
// has been changed. Avoid accessing to invalid objects that could
@@ -278,16 +289,18 @@ export default class EventsBasedBehaviorsList extends React.Component<
height={height}
onAddNewItem={this._addNewEventsBasedBehavior}
addNewItemLabel={<Trans>Add a new behavior</Trans>}
selectedItem={selectedEventsBasedBehavior}
getItemName={getEventsBasedBehaviorName}
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

@@ -26,6 +26,9 @@ const styles = {
},
};
const getEventsFunctionName = (eventsFunction: gdEventsFunction) =>
eventsFunction.getName();
type State = {|
renamedEventsFunction: ?gdEventsFunction,
searchText: string,
@@ -97,7 +100,9 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
{
renamedEventsFunction: eventsFunction,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -121,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) => {
@@ -243,15 +257,10 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
} = this.props;
const { searchText } = this.state;
const list = [
...filterEventFunctionsList(
enumerateEventsFunctions(eventsFunctionsContainer),
searchText
),
{
key: 'add-item-row',
},
];
const list = filterEventFunctionsList(
enumerateEventsFunctions(eventsFunctionsContainer),
searchText
);
// Force List component to be mounted again if project or objectsContainer
// has been changed. Avoid accessing to invalid objects that could
@@ -272,16 +281,16 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
height={height}
onAddNewItem={this._addNewEventsFunction}
addNewItemLabel={<Trans>Add a new function</Trans>}
selectedItem={selectedEventsFunction}
getItemName={getEventsFunctionName}
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

@@ -17,19 +17,10 @@ import {
type InstructionContext,
} from '../SelectionHandler';
import InstructionsList from './InstructionsList';
import {
DragSource,
type DragSourceMonitor,
type DragSourceConnector,
type ConnectDragSource,
DropTarget,
type DropTargetMonitor,
type DropTargetConnector,
type ConnectDropTarget,
} from 'react-dnd';
import DropIndicator from './DropIndicator';
import ParameterRenderingService from '../ParameterRenderingService';
import InvalidParameterValue from './InvalidParameterValue';
import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndDropTarget';
const gd = global.gd;
const instrFormatter = gd.InstructionSentenceFormatter.get();
instrFormatter.loadTypesFormattingFromConfig();
@@ -43,15 +34,11 @@ const styles = {
},
};
type DragSourceProps = {|
connectDragSource: ConnectDragSource,
|};
export const reactDndInstructionType = 'GD_DRAGGED_INSTRUCTION';
type DropTargetProps = {|
connectDropTarget: ConnectDropTarget,
isOver: boolean,
canDrop: boolean,
|};
const DragSourceAndDropTarget = makeDragSourceAndDropTarget<{
isCondition: boolean,
}>(reactDndInstructionType);
type Props = {|
instruction: gdInstruction,
@@ -63,9 +50,6 @@ type Props = {|
onContextMenu: (x: number, y: number) => void,
onMoveToInstruction: () => void,
...DragSourceProps,
...DropTargetProps,
// For potential sub-instructions list:
selection: Object,
onAddNewSubInstruction: InstructionsListContext => void,
@@ -91,7 +75,7 @@ type Props = {|
renderObjectThumbnail: string => React.Node,
|};
class Instruction extends React.Component<Props, *> {
export default class Instruction extends React.Component<Props> {
/**
* Render the different parts of the text of the instruction.
* Parameter can have formatting, be hovered and clicked. The rest
@@ -156,12 +140,11 @@ class Instruction extends React.Component<Props, *> {
};
render() {
const { instruction, isCondition } = this.props;
const {
connectDragSource,
connectDropTarget,
isOver,
canDrop,
instruction,
isCondition,
onClick,
onMoveToInstruction,
} = this.props;
//TODO: Metadata could be cached for performance boost.
@@ -175,158 +158,121 @@ class Instruction extends React.Component<Props, *> {
instruction.getType()
);
const instructionDiv = connectDropTarget(
// $FlowFixMe
connectDragSource(
<div
style={styles.container}
className={classNames({
[selectableArea]: true,
[selectedArea]: this.props.selected,
})}
onClick={e => {
e.stopPropagation();
this.props.onClick();
}}
onDoubleClick={e => {
e.stopPropagation();
this.props.onDoubleClick();
}}
onContextMenu={e => {
e.stopPropagation();
this.props.onContextMenu(e.clientX, e.clientY);
}}
onKeyPress={event => {
if (event.key === 'Enter') {
this.props.onDoubleClick();
event.stopPropagation();
event.preventDefault();
} else if (event.key === ' ') {
this.props.onClick();
event.stopPropagation();
event.preventDefault();
}
}}
tabIndex={0}
>
{instruction.isInverted() && (
<img
className={classNames({
[icon]: true,
})}
src="res/contraire.png"
alt="Condition is negated"
/>
)}
<img
className={classNames({
[icon]: true,
})}
src={metadata.getSmallIconFilename()}
alt=""
/>
{this._renderInstructionText(metadata)}
{metadata.canHaveSubInstructions() && (
<InstructionsList
style={
{} /* TODO: Use a new object to force update - somehow updates are not always propagated otherwise */
}
extraClassName={subInstructionsContainer}
instrsList={instruction.getSubInstructions()}
areConditions={this.props.isCondition}
selection={this.props.selection}
onAddNewInstruction={this.props.onAddNewSubInstruction}
onPasteInstructions={this.props.onPasteSubInstructions}
onMoveToInstruction={this.props.onMoveToSubInstruction}
onMoveToInstructionsList={this.props.onMoveToSubInstructionsList}
onInstructionClick={this.props.onSubInstructionClick}
onInstructionDoubleClick={this.props.onSubInstructionDoubleClick}
onInstructionContextMenu={this.props.onSubInstructionContextMenu}
onInstructionsListContextMenu={
this.props.onSubInstructionsListContextMenu
}
onParameterClick={this.props.onSubParameterClick}
addButtonLabel={<Trans>Add a sub-condition</Trans>}
disabled={this.props.disabled}
renderObjectThumbnail={this.props.renderObjectThumbnail}
/>
)}
</div>
)
);
return (
<DragSourceAndDropTarget
beginDrag={() => {
onClick(); // Select the dragged instruction
return isOver ? (
<React.Fragment>
<DropIndicator canDrop={canDrop} />
{instructionDiv}
</React.Fragment>
) : (
instructionDiv || null
// No need to save here what is being dragged,
// as its the entire selection that is considered to be dragged.
return {
isCondition,
};
}}
canDrop={draggedItem => draggedItem.isCondition === isCondition}
drop={() => {
onMoveToInstruction();
}}
>
{({ connectDragSource, connectDropTarget, isOver, canDrop }) => {
// The instruction itself can be dragged and is a target for
// another instruction to be dropped. It's IMPORTANT NOT to have
// the subinstructions list inside the connectDropTarget/connectDragSource
// as otherwise this can confuse react-dnd ("Expected to find a valid target")
// (surely due to components re-mounting/rerendering ?).
const instructionElement = connectDropTarget(
connectDragSource(
<div
style={styles.container}
className={classNames({
[selectableArea]: true,
[selectedArea]: this.props.selected,
})}
onClick={e => {
e.stopPropagation();
this.props.onClick();
}}
onDoubleClick={e => {
e.stopPropagation();
this.props.onDoubleClick();
}}
onContextMenu={e => {
e.stopPropagation();
this.props.onContextMenu(e.clientX, e.clientY);
}}
onKeyPress={event => {
if (event.key === 'Enter') {
this.props.onDoubleClick();
event.stopPropagation();
event.preventDefault();
} else if (event.key === ' ') {
this.props.onClick();
event.stopPropagation();
event.preventDefault();
}
}}
tabIndex={0}
>
{instruction.isInverted() && (
<img
className={classNames({
[icon]: true,
})}
src="res/contraire.png"
alt="Condition is negated"
/>
)}
<img
className={classNames({
[icon]: true,
})}
src={metadata.getSmallIconFilename()}
alt=""
/>
{this._renderInstructionText(metadata)}
</div>
)
);
return (
<React.Fragment>
{isOver && <DropIndicator canDrop={canDrop} />}
{instructionElement}
{metadata.canHaveSubInstructions() && (
<InstructionsList
style={
{} /* TODO: Use a new object to force update - somehow updates are not always propagated otherwise */
}
extraClassName={subInstructionsContainer}
instrsList={instruction.getSubInstructions()}
areConditions={this.props.isCondition}
selection={this.props.selection}
onAddNewInstruction={this.props.onAddNewSubInstruction}
onPasteInstructions={this.props.onPasteSubInstructions}
onMoveToInstruction={this.props.onMoveToSubInstruction}
onMoveToInstructionsList={
this.props.onMoveToSubInstructionsList
}
onInstructionClick={this.props.onSubInstructionClick}
onInstructionDoubleClick={
this.props.onSubInstructionDoubleClick
}
onInstructionContextMenu={
this.props.onSubInstructionContextMenu
}
onInstructionsListContextMenu={
this.props.onSubInstructionsListContextMenu
}
onParameterClick={this.props.onSubParameterClick}
addButtonLabel={<Trans>Add a sub-condition</Trans>}
disabled={this.props.disabled}
renderObjectThumbnail={this.props.renderObjectThumbnail}
/>
)}
</React.Fragment>
);
}}
</DragSourceAndDropTarget>
);
}
}
// Drag'n'drop support:
export const reactDndInstructionType = 'GD_DRAGGED_INSTRUCTION';
type InstructionSourceProps = {
onClick: () => void,
isCondition: boolean,
};
const instructionSource = {
beginDrag(props: InstructionSourceProps) {
props.onClick(); // Select the dragged instruction
return {
// No need to save here what is being dragged,
// as its the entire selection that is considered to be dragged.
isCondition: props.isCondition,
};
},
};
function sourceCollect(
connect: DragSourceConnector,
monitor: DragSourceMonitor
): DragSourceProps {
return {
connectDragSource: connect.dragSource(),
};
}
const instructionTarget = {
canDrop(props: Props, monitor: DropTargetMonitor) {
return (
monitor.getItem() && monitor.getItem().isCondition === props.isCondition
);
},
drop(props: Props, monitor: DropTargetMonitor) {
if (monitor.didDrop()) {
return; // Drop already handled by a subinstruction
}
props.onMoveToInstruction();
},
};
function targetCollect(
connect: DropTargetConnector,
monitor: DropTargetMonitor
): DropTargetProps {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
};
}
// $FlowFixMe - Typing of DragSource/DropTarget is a pain to get correctly
export default DragSource(
reactDndInstructionType,
instructionSource,
sourceCollect
)(
DropTarget(reactDndInstructionType, instructionTarget, targetCollect)(
Instruction
)
);

View File

@@ -9,15 +9,10 @@ import {
type ParameterContext,
} from '../SelectionHandler';
import { actionsContainer, conditionsContainer } from './ClassNames';
import {
DropTarget,
type DropTargetMonitor,
type DropTargetConnector,
type ConnectDropTarget,
} from 'react-dnd';
import DropIndicator from './DropIndicator';
import { Trans } from '@lingui/macro';
import { hasClipboardConditions, hasClipboardActions } from '../ClipboardKind';
import { makeDropTarget } from '../../UI/DragAndDrop/DropTarget';
const styles = {
addButton: {
@@ -25,12 +20,6 @@ const styles = {
},
};
type DropTargetProps = {|
connectDropTarget: ConnectDropTarget,
isOver: boolean,
canDrop: boolean,
|};
type Props = {
instrsList: gdInstructionsList,
areConditions: boolean,
@@ -55,14 +44,17 @@ type Props = {
style?: Object,
disabled: boolean,
renderObjectThumbnail: string => React.Node,
...DropTargetProps,
};
type State = {|
canPaste: boolean,
|};
class InstructionsList extends React.Component<Props, State> {
const DropTarget = makeDropTarget<{
isCondition: boolean,
}>(reactDndInstructionType);
export default class InstructionsList extends React.Component<Props, State> {
state = { canPaste: false };
onAddNewInstruction = () => {
@@ -101,8 +93,6 @@ class InstructionsList extends React.Component<Props, State> {
disabled,
} = this.props;
const { connectDropTarget, isOver, canDrop } = this.props;
const instructions = mapFor(0, instrsList.size(), i => {
const instruction = instrsList.get(i);
const instructionContext = {
@@ -113,7 +103,6 @@ class InstructionsList extends React.Component<Props, State> {
};
return (
// $FlowFixMe - Flow don't see that DropTarget hoc is being used in instructions?
<Instruction
instruction={instruction}
isCondition={areConditions}
@@ -160,96 +149,72 @@ class InstructionsList extends React.Component<Props, State> {
) : (
<Trans>Add action</Trans>
);
const instructionsList = connectDropTarget(
<div
className={`${
areConditions ? conditionsContainer : actionsContainer
} ${extraClassName || ''}`}
style={style}
>
{instructions}
{isOver && <DropIndicator canDrop={canDrop} />}
<span
onPointerEnter={() => {
const canPaste =
(areConditions && hasClipboardConditions()) ||
(!areConditions && hasClipboardActions());
this.setState({ canPaste });
}}
onPointerLeave={() => this.setState({ canPaste: false })}
>
<button
style={styles.addButton}
className="add-link"
onClick={this.onAddNewInstruction}
onContextMenu={e => {
e.stopPropagation();
onInstructionsListContextMenu(
e.clientX,
e.clientY,
instructionsListContext
);
}}
>
{addButtonLabel || addButtonDefaultLabel}
</button>
{canPaste && (
<span>
{' '}
<button
style={styles.addButton}
className="add-link"
onClick={this._onPasteInstructions}
>
{areConditions ? (
<Trans>(or paste conditions)</Trans>
) : (
<Trans>(or paste actions)</Trans>
)}
</button>
</span>
)}
</span>
</div>
);
return instructionsList || null;
return (
<DropTarget
canDrop={draggedItem => draggedItem.isCondition === areConditions}
drop={() => {
onMoveToInstructionsList({
isCondition: areConditions,
instrsList: instrsList,
});
}}
>
{({ connectDropTarget, isOver, canDrop }) =>
connectDropTarget(
<div
className={`${
areConditions ? conditionsContainer : actionsContainer
} ${extraClassName || ''}`}
style={style}
>
{instructions}
{isOver && <DropIndicator canDrop={canDrop} />}
<span
onPointerEnter={() => {
const canPaste =
(areConditions && hasClipboardConditions()) ||
(!areConditions && hasClipboardActions());
this.setState({ canPaste });
}}
onPointerLeave={() => this.setState({ canPaste: false })}
>
<button
style={styles.addButton}
className="add-link"
onClick={this.onAddNewInstruction}
onContextMenu={e => {
e.stopPropagation();
onInstructionsListContextMenu(
e.clientX,
e.clientY,
instructionsListContext
);
}}
>
{addButtonLabel || addButtonDefaultLabel}
</button>
{canPaste && (
<span>
{' '}
<button
style={styles.addButton}
className="add-link"
onClick={this._onPasteInstructions}
>
{areConditions ? (
<Trans>(or paste conditions)</Trans>
) : (
<Trans>(or paste actions)</Trans>
)}
</button>
</span>
)}
</span>
</div>
)
}
</DropTarget>
);
}
}
// Drag'n'drop support:
const instructionsListTarget = {
canDrop(props: Props, monitor: DropTargetMonitor) {
return (
monitor.getItem() && monitor.getItem().isCondition === props.areConditions
);
},
drop(props: Props, monitor: DropTargetMonitor) {
if (monitor.didDrop()) {
return; // Drop already handled by an instruction
}
props.onMoveToInstructionsList({
isCondition: props.areConditions,
instrsList: props.instrsList,
});
},
};
function targetCollect(
connect: DropTargetConnector,
monitor: DropTargetMonitor
): DropTargetProps {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
};
}
// $FlowFixMe - Typing of DragSource/DropTarget is a pain to get correctly
export default DropTarget(
reactDndInstructionType,
instructionsListTarget,
targetCollect
)(InstructionsList);

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

@@ -0,0 +1,146 @@
// @flow
import { roundPosition } from '../Utils/GridHelpers';
const gd = global.gd;
type Props = {|
instances: gdInitialInstancesContainer,
options: Object,
|};
const roundPositionsToGrid = (
pos: [number, number],
options: Object
): [number, number] => {
const newPos = pos;
if (options.grid && options.snap) {
newPos[0] = roundPosition(
newPos[0],
options.gridWidth,
options.gridOffsetX
);
newPos[1] = roundPosition(
newPos[1],
options.gridHeight,
options.gridOffsetY
);
}
return newPos;
};
/**
* Allow to add instances on the scene. Supports "temporary" instances,
* which are real instances but can be deleted as long as they are not "committed".
*/
export default class InstancesAdder {
_instances: gdInitialInstancesContainer;
_temporaryInstances: Array<gdInitialInstance>;
_options: Object;
_zOrderFinder = new gd.HighestZOrderFinder();
constructor({ instances, options }: Props) {
this._instances = instances;
this._options = options;
this._temporaryInstances = [];
}
setOptions(options: Object) {
this._options = options;
}
/**
* Immediately create new instance at the specified position
* (specified in scene coordinates).
*/
addInstances = (pos: [number, number], objectNames: Array<string>) => {
this._instances.iterateOverInstances(this._zOrderFinder);
const zOrder = this._zOrderFinder.getHighestZOrder() + 1;
const newPos = roundPositionsToGrid(pos, this._options);
objectNames.map(objectName => {
const instance: gdInitialInstance = this._instances.insertNewInitialInstance();
instance.setObjectName(objectName);
instance.setX(newPos[0]);
instance.setY(newPos[1]);
instance.setZOrder(zOrder);
return instance;
});
};
/**
* Create temporary instances at the specified position
* (specified in scene coordinates).
*/
createOrUpdateTemporaryInstancesFromObjectNames = (
pos: [number, number],
objectNames: Array<string>
) => {
if (!objectNames.length) return;
if (!this._temporaryInstances.length) {
this._createTemporaryInstancesFromObjectNames(pos, objectNames);
} else {
this.updateTemporaryInstancePositions(pos);
}
};
_createTemporaryInstancesFromObjectNames = (
pos: [number, number],
objectNames: Array<string>
) => {
this.deleteTemporaryInstances();
this._instances.iterateOverInstances(this._zOrderFinder);
const zOrder = this._zOrderFinder.getHighestZOrder() + 1;
const newPos = roundPositionsToGrid(pos, this._options);
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);
return instance;
});
};
/**
* Update the temporary instances positions
* (specified in scene coordinates). Useful when dragging these instances.
*/
updateTemporaryInstancePositions = (pos: [number, number]) => {
const newPos = roundPositionsToGrid(pos, this._options);
this._temporaryInstances.forEach(instance => {
instance.setX(newPos[0]);
instance.setY(newPos[1]);
});
};
/**
* Delete the temporary instances.
*/
deleteTemporaryInstances() {
this._temporaryInstances.forEach(instance => {
this._instances.removeInstance(instance);
});
this._temporaryInstances = [];
}
/**
* Consider the temporary instances as not temporary anymore.
*/
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

@@ -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

@@ -2,7 +2,7 @@
import * as PIXI from 'pixi.js';
type Props = {
getLastCursorPosition: () => [number, number],
getLastCursorSceneCoordinates: () => [number, number],
width: number,
height: number,
};
@@ -10,13 +10,13 @@ type Props = {
export default class StatusBar {
_width: number;
_height: number;
_getLastCursorPosition: () => [number, number];
_getLastCursorSceneCoordinates: () => [number, number];
_statusBarContainer: PIXI.Container;
_statusBarBackground: PIXI.Graphics;
_statusBarText: PIXI.Text;
constructor({ getLastCursorPosition, width, height }: Props) {
this._getLastCursorPosition = getLastCursorPosition;
constructor({ getLastCursorSceneCoordinates, width, height }: Props) {
this._getLastCursorSceneCoordinates = getLastCursorSceneCoordinates;
this._statusBarContainer = new PIXI.Container();
this._statusBarContainer.alpha = 0.8;
this._statusBarContainer.hitArea = new PIXI.Rectangle(0, 0, 0, 0);
@@ -43,7 +43,7 @@ export default class StatusBar {
render() {
const padding = 5;
const borderRadius = 4;
const [x, y] = this._getLastCursorPosition();
const [x, y] = this._getLastCursorSceneCoordinates();
this._statusBarText.text = `${x.toFixed(0)};${y.toFixed(0)}`;
this._statusBarText.position.x = 0 + padding;
this._statusBarText.position.y = Math.round(

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 '../Utils/DragDropHelpers/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 InstancesAdder from './InstancesAdder';
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,16 @@ 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._instancesAdder = new InstancesAdder({
instances: this.props.initialInstances,
options: this.props.options,
});
this._mountEditorComponents(this.props);
this._renderScene();
}
@@ -278,7 +267,7 @@ export default class InstancesEditorContainer extends Component {
this.statusBar = new StatusBar({
width: this.props.width,
height: this.props.height,
getLastCursorPosition: this.getLastCursorPosition,
getLastCursorSceneCoordinates: this.getLastCursorSceneCoordinates,
});
this.pixiContainer.addChild(this.selectionRectangle.getPixiObject());
@@ -300,6 +289,7 @@ export default class InstancesEditorContainer extends Component {
this.keyboardShortcuts.unmount();
this.selectionRectangle.delete();
this.instancesRenderer.delete();
this._instancesAdder.unmount();
if (this.nextFrame) cancelAnimationFrame(this.nextFrame);
stopPIXITicker();
}
@@ -330,6 +320,7 @@ export default class InstancesEditorContainer extends Component {
this.instancesResizer.setOptions(nextProps.options);
this.windowMask.setOptions(nextProps.options);
this.viewPosition.setOptions(nextProps.options);
this._instancesAdder.setOptions(nextProps.options);
}
if (
@@ -375,6 +366,17 @@ export default class InstancesEditorContainer extends Component {
});
}
/**
* Immediately add instances for the specified objects at the given
* position (in scene coordinates).
*/
addInstances = (
pos /*: [number, number] */,
objectNames /*: Array<string> */
) => {
this._instancesAdder.addInstances(pos, objectNames);
};
_onMouseMove = (x, y) => {
this.lastCursorX = x;
this.lastCursorY = y;
@@ -385,6 +387,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 +414,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 +563,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);
};
@@ -593,14 +595,14 @@ export default class InstancesEditorContainer extends Component {
}
}
getLastContextMenuPosition = () => {
getLastContextMenuSceneCoordinates = () => {
return this.viewPosition.toSceneCoordinates(
this.lastContextMenuX,
this.lastContextMenuY
);
};
getLastCursorPosition = () => {
getLastCursorSceneCoordinates = () => {
return this.viewPosition.toSceneCoordinates(
this.lastCursorX,
this.lastCursorY
@@ -652,15 +654,63 @@ 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 { _instancesAdder, viewPosition, canvasArea } = this;
if (!_instancesAdder || !canvasArea || !viewPosition) return;
const { x, y } = monitor.getClientOffset();
const canvasRect = canvasArea.getBoundingClientRect();
const pos = viewPosition.toSceneCoordinates(
x - canvasRect.left,
y - canvasRect.top
);
_instancesAdder.createOrUpdateTemporaryInstancesFromObjectNames(
pos,
this.props.selectedObjectNames
);
}}
drop={monitor => {
const { _instancesAdder, viewPosition, canvasArea } = this;
if (!_instancesAdder || !canvasArea || !viewPosition) return;
if (monitor.didDrop()) {
// Drop was done somewhere else (in a child of the canvas:
// should not happen, but still handling this case).
_instancesAdder.deleteTemporaryInstances();
return;
}
const { x, y } = monitor.getClientOffset();
const canvasRect = canvasArea.getBoundingClientRect();
const pos = viewPosition.toSceneCoordinates(
x - canvasRect.left,
y - canvasRect.top
);
_instancesAdder.updateTemporaryInstancePositions(pos);
_instancesAdder.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._instancesAdder && !isOver) {
this._instancesAdder.deleteTemporaryInstances();
}
return connectDropTarget(
<div
ref={canvasArea => (this.canvasArea = canvasArea)}
style={{
...styles.canvasArea,
...(this.props.showDropCursor ? styles.dropCursor : undefined),
}}
/>
);
}}
</DropTarget>
);
}
}

View File

@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import DragDropContextProvider from '../Utils/DragDropHelpers/DragDropContextProvider';
import DragAndDropContextProvider from '../UI/DragAndDrop/DragAndDropContextProvider';
import { ThemeProvider } from '@material-ui/styles';
import { getTheme } from '../UI/Theme';
import UserProfileProvider from '../Profile/UserProfileProvider';
@@ -48,7 +48,7 @@ export default class Providers extends React.Component<Props, {||}> {
eventsFunctionsExtensionOpener,
} = this.props;
return (
<DragDropContextProvider>
<DragAndDropContextProvider>
<PreferencesProvider disableCheckForUpdates={disableCheckForUpdates}>
<PreferencesContext.Consumer>
{({ values }) => {
@@ -91,7 +91,7 @@ export default class Providers extends React.Component<Props, {||}> {
}}
</PreferencesContext.Consumer>
</PreferencesProvider>
</DragDropContextProvider>
</DragAndDropContextProvider>
);
}
}

View File

@@ -83,6 +83,8 @@ import LanguageDialog from './Preferences/LanguageDialog';
import PreferencesContext from './Preferences/PreferencesContext';
import { getFunctionNameFromType } from '../EventsFunctionsExtensionsLoader';
import { type ExportDialogWithoutExportsProps } from '../Export/ExportDialog';
import { getStartupTimesSummary } from '../Utils/StartupTimes';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
const gd = global.gd;
@@ -184,6 +186,8 @@ class MainFrame extends React.Component<Props, State> {
}
componentDidMount() {
GD_STARTUP_TIMES.push(['MainFrameComponentDidMount', performance.now()]);
const { initialPathsOrURLsToOpen } = this.props;
this._loadExtensions();
@@ -193,6 +197,9 @@ class MainFrame extends React.Component<Props, State> {
);
} else if (this.props.introDialog && !Window.isDev())
this._openIntroDialog(true);
GD_STARTUP_TIMES.push(['MainFrameComponentDidMountFinished', performance.now()]);
console.info("Startup times:", getStartupTimesSummary());
}
_languageDidChange() {

View File

@@ -227,7 +227,9 @@ export default class GroupsListContainer extends React.Component<Props, State> {
{
renamedGroupWithScope: groupWithContext,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -285,7 +287,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

@@ -26,6 +26,17 @@ export type GroupWithContext = {|
export type ObjectWithContextList = Array<ObjectWithContext>;
export type GroupWithContextList = Array<GroupWithContext>;
export const isSameObjectWithContext = (
objectWithContext: ?ObjectWithContext
) => (other: ?ObjectWithContext) => {
return (
objectWithContext &&
other &&
objectWithContext.global === other.global &&
objectWithContext.object === other.object
);
};
export const enumerateObjects = (
project: gdProject,
objectsContainer: gdObjectsContainer,

View File

@@ -1,11 +1,11 @@
// @flow
import { Trans } from '@lingui/macro';
import React, { Component } from 'react';
import { AutoSizer, List } from 'react-virtualized';
import React from 'react';
import { AutoSizer } from 'react-virtualized';
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
import Background from '../UI/Background';
import SearchBar from '../UI/SearchBar';
import ObjectRow from './ObjectRow';
import NewObjectDialog from './NewObjectDialog';
import VariablesEditorDialog from '../VariablesList/VariablesEditorDialog';
import newNameGenerator from '../Utils/NewNameGenerator';
@@ -15,9 +15,11 @@ import {
unserializeFromJSObject,
} from '../Utils/Serializer';
import { showWarningBox } from '../UI/Messages/MessageBox';
import { AddListItem } from '../UI/ListCommonItem';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import { enumerateObjects, filterObjectsList } from './EnumerateObjects';
import {
enumerateObjects,
filterObjectsList,
isSameObjectWithContext,
} from './EnumerateObjects';
import type {
ObjectWithContextList,
ObjectWithContext,
@@ -25,8 +27,13 @@ import type {
import { CLIPBOARD_KIND } from './ClipboardKind';
import TagChips from '../UI/TagChips';
import EditTagsDialog from '../UI/EditTagsDialog';
import { type Tags, getStringFromTags } from '../Utils/TagsHelper';
import { listItemWith32PxIconHeight } from '../UI/List';
import {
type Tags,
type SelectedTags,
getStringFromTags,
buildTagsMenuTemplate,
getTagsFromString,
} from '../Utils/TagsHelper';
const styles = {
listContainer: {
@@ -34,120 +41,29 @@ const styles = {
},
};
const SortableObjectRow = SortableElement(props => {
const { style, ...otherProps } = props;
return (
<div style={style}>
<ObjectRow {...otherProps} />
</div>
);
});
export const objectWithContextReactDndType = 'GD_OBJECT_WITH_CONTEXT';
const SortableAddObjectRow = SortableElement(props => {
const { style, ...otherProps } = props;
return (
<div style={style}>
<AddListItem {...otherProps} />
</div>
);
});
const getObjectWithContextName = (objectWithContext: ObjectWithContext) =>
objectWithContext.object.getName();
class ObjectsList extends Component<*, *> {
list: any;
const isObjectWithContextGlobal = (objectWithContext: ObjectWithContext) =>
objectWithContext.global;
forceUpdateGrid() {
if (this.list) this.list.forceUpdateGrid();
const getPasteLabel = isGlobalObject => {
let clipboardObjectName = '';
if (Clipboard.has(CLIPBOARD_KIND)) {
const clipboardContent = Clipboard.get(CLIPBOARD_KIND);
if (clipboardContent) {
clipboardObjectName = clipboardContent.name;
}
}
render() {
let { height, width, fullList, project, selectedObjectNames } = this.props;
return isGlobalObject
? 'Paste ' + clipboardObjectName + ' as a Global Object'
: 'Paste ' + clipboardObjectName;
};
return (
<List
ref={list => (this.list = list)}
height={height}
rowCount={fullList.length}
rowHeight={listItemWith32PxIconHeight}
rowRenderer={({ index, key, style }) => {
const objectWithContext = fullList[index];
if (objectWithContext.key === 'add-objects-row') {
return (
<SortableAddObjectRow
index={fullList.length}
key={key}
style={style}
disabled
onClick={this.props.onAddNewObject}
primaryText={<Trans>Click to add an object</Trans>}
/>
);
}
const nameBeingEdited =
this.props.renamedObjectWithContext &&
this.props.renamedObjectWithContext.object ===
objectWithContext.object &&
this.props.renamedObjectWithContext.global ===
objectWithContext.global;
return (
<SortableObjectRow
index={index}
key={objectWithContext.object.ptr}
project={project}
object={objectWithContext.object}
isGlobalObject={objectWithContext.global}
style={style}
onEdit={
this.props.onEditObject
? () => this.props.onEditObject(objectWithContext.object)
: undefined
}
onEditVariables={() =>
this.props.onEditVariables(objectWithContext.object)
}
onEditName={() => this.props.onEditName(objectWithContext)}
onDelete={() => this.props.onDelete(objectWithContext)}
onCopyObject={() => this.props.onCopyObject(objectWithContext)}
onCutObject={() => this.props.onCutObject(objectWithContext)}
onDuplicateObject={() =>
this.props.onDuplicateObject(objectWithContext)
}
onPasteObject={() => this.props.onPasteObject(objectWithContext)}
onRename={newName =>
this.props.onRename(objectWithContext, newName)
}
onSetAsGlobalObject={
objectWithContext.global
? undefined
: () => this.props.onSetAsGlobalObject(objectWithContext)
}
onAddNewObject={this.props.onAddNewObject}
editingName={nameBeingEdited}
getThumbnail={this.props.getThumbnail}
getAllObjectTags={this.props.getAllObjectTags}
onObjectSelected={this.props.onObjectSelected}
onEditTags={() => this.props.onEditTags(objectWithContext.object)}
onChangeTags={objectTags =>
this.props.onChangeTags(objectWithContext.object, objectTags)
}
selected={
selectedObjectNames.indexOf(
objectWithContext.object.getName()
) !== -1
}
/>
);
}}
width={width}
/>
);
}
}
const SortableObjectsList = SortableContainer(ObjectsList, { withRef: true });
type StateType = {|
type State = {|
newObjectDialogOpen: boolean,
renamedObjectWithContext: ?ObjectWithContext,
variablesEditedObject: ?gdObject,
@@ -155,23 +71,37 @@ type StateType = {|
tagEditedObject: ?gdObject,
|};
export default class ObjectsListContainer extends React.Component<
*,
StateType
> {
static defaultProps = {
onDeleteObject: (objectWithContext: ObjectWithContext, cb: Function) =>
cb(true),
onRenameObject: (
objectWithContext: ObjectWithContext,
newName: string,
cb: Function
) => cb(true),
};
type Props = {|
project: gdProject,
objectsContainer: gdObjectsContainer,
onDeleteObject: (
objectWithContext: ObjectWithContext,
cb: (boolean) => void
) => void,
onRenameObject: (
objectWithContext: ObjectWithContext,
newName: string,
cb: (boolean) => void
) => void,
selectedObjectNames: Array<string>,
sortableList: any;
_displayedObjectsList: ObjectWithContextList = [];
state: StateType = {
selectedObjectTags: SelectedTags,
getAllObjectTags: () => Tags,
onChangeSelectedObjectTags: SelectedTags => void,
onEditObject: gdObject => void,
onObjectCreated: string => void,
onObjectSelected: string => void,
onObjectPasted?: gdObject => void,
canRenameObject: (newName: string) => boolean,
getThumbnail: (project: gdProject, object: Object) => string,
|};
export default class ObjectsList extends React.Component<Props, State> {
sortableList: ?SortableVirtualizedItemList<ObjectWithContext>;
_displayedObjectWithContextsList: ObjectWithContextList = [];
state = {
newObjectDialogOpen: false,
renamedObjectWithContext: null,
variablesEditedObject: null,
@@ -179,7 +109,7 @@ export default class ObjectsListContainer extends React.Component<
tagEditedObject: null,
};
shouldComponentUpdate(nextProps: *, nextState: StateType) {
shouldComponentUpdate(nextProps: Props, nextState: State) {
// The component is costly to render, so avoid any re-rendering as much
// as possible.
// We make the assumption that no changes to objects list is made outside
@@ -351,7 +281,9 @@ export default class ObjectsListContainer extends React.Component<
{
renamedObjectWithContext: objectWithContext,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -378,48 +310,61 @@ export default class ObjectsListContainer extends React.Component<
}
};
_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._displayedObjectsList[oldIndex];
const destinationObjectWithContext = this._displayedObjectsList[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
);
return (
selectedObjects.filter(movedObjectWithContext => {
return (
movedObjectWithContext.global === destinationObjectWithContext.global
);
}).length > 0
);
this.forceUpdateList();
};
_onStartDraggingObject = ({ index }: { index: number }) => {
const draggedObjectWithContext = this._displayedObjectsList[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) => {
const { object } = objectWithContext;
const { project, objectsContainer } = this.props;
const objectName = object.getName();
const objectName: string = object.getName();
if (!objectsContainer.hasObjectNamed(objectName)) return;
if (project.hasObjectNamed(objectName)) {
@@ -449,7 +394,7 @@ export default class ObjectsListContainer extends React.Component<
forceUpdateList = () => {
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
if (this.sortableList) this.sortableList.forceUpdateGrid();
};
_openEditTagDialog = (tagEditedObject: ?gdObject) => {
@@ -466,19 +411,102 @@ export default class ObjectsListContainer extends React.Component<
this.forceUpdateList();
};
_selectObject = (objectWithContext: ?ObjectWithContext) => {
this.props.onObjectSelected(
objectWithContext ? objectWithContext.object.getName() : ''
);
};
_getObjectThumbnail = (objectWithContext: ObjectWithContext) =>
this.props.getThumbnail(this.props.project, objectWithContext.object);
_renderObjectMenuTemplate = (
objectWithContext: ObjectWithContext,
index: number
) => {
const { object } = objectWithContext;
return [
{
label: 'Edit object',
click: () => this.props.onEditObject(object),
},
{
label: 'Edit object variables',
click: () => this._editVariables(object),
},
{ type: 'separator' },
{
label: 'Tags',
submenu: buildTagsMenuTemplate({
noTagLabel: 'No tags',
getAllTags: this.props.getAllObjectTags,
selectedTags: getTagsFromString(object.getTags()),
onChange: objectTags => {
this._changeObjectTags(object, objectTags);
},
editTagsLabel: 'Add/edit tags...',
onEditTags: () => this._openEditTagDialog(object),
}),
},
{
label: 'Rename',
click: () => this._editName(objectWithContext),
},
{
label: 'Set as a global object',
click: () => this._setAsGlobalObject(objectWithContext),
},
{
label: 'Delete',
click: () => this._deleteObject(objectWithContext),
},
{ type: 'separator' },
{
label: 'Add a new object...',
click: () => this.onAddNewObject(),
},
{ type: 'separator' },
{
label: 'Copy',
click: () => this._copyObject(objectWithContext),
},
{
label: 'Cut',
click: () => this._cutObject(objectWithContext),
},
{
label: getPasteLabel(objectWithContext.global),
enabled: Clipboard.has(CLIPBOARD_KIND),
click: () => this._paste(objectWithContext),
},
{
label: 'Duplicate',
click: () => this._duplicateObject(objectWithContext),
},
];
};
render() {
const { project, objectsContainer, selectedObjectTags } = this.props;
const { searchText, tagEditedObject } = this.state;
const lists = enumerateObjects(project, objectsContainer);
this._displayedObjectsList = filterObjectsList(lists.allObjectsList, {
searchText,
selectedTags: selectedObjectTags,
});
const fullList = this._displayedObjectsList.concat({
key: 'add-objects-row',
object: null,
});
this._displayedObjectWithContextsList = filterObjectsList(
lists.allObjectsList,
{
searchText,
selectedTags: selectedObjectTags,
}
);
const selectedObjects = this._displayedObjectWithContextsList.filter(
objectWithContext =>
this.props.selectedObjectNames.indexOf(
objectWithContext.object.getName()
) !== -1
);
const renamedObjectWithContext = this._displayedObjectWithContextsList.find(
isSameObjectWithContext(this.state.renamedObjectWithContext)
);
// Force List component to be mounted again if project or objectsContainer
// has been changed. Avoid accessing to invalid objects that could
@@ -494,38 +522,28 @@ export default class ObjectsListContainer extends React.Component<
<div style={styles.listContainer}>
<AutoSizer>
{({ height, width }) => (
<SortableObjectsList
<SortableVirtualizedItemList
key={listKey}
ref={sortableList => (this.sortableList = sortableList)}
fullList={fullList}
project={project}
fullList={this._displayedObjectWithContextsList}
width={width}
height={height}
renamedObjectWithContext={this.state.renamedObjectWithContext}
getThumbnail={this.props.getThumbnail}
getAllObjectTags={this.props.getAllObjectTags}
onEditTags={this._openEditTagDialog}
onChangeTags={this._changeObjectTags}
selectedObjectNames={this.props.selectedObjectNames}
onObjectSelected={this.props.onObjectSelected}
onEditObject={this.props.onEditObject}
onCopyObject={this._copyObject}
onCutObject={this._cutObject}
onDuplicateObject={this._duplicateObject}
onSetAsGlobalObject={this._setAsGlobalObject}
onPasteObject={this._pasteAndRename}
onAddNewObject={this.onAddNewObject}
onEditName={this._editName}
onEditVariables={this._editVariables}
onDelete={this._deleteObject}
getItemName={getObjectWithContextName}
getItemThumbnail={this._getObjectThumbnail}
isItemBold={isObjectWithContextGlobal}
onEditItem={objectWithContext =>
this.props.onEditObject(objectWithContext.object)
}
onAddNewItem={this.onAddNewObject}
addNewItemLabel={<Trans>Add a new object</Trans>}
selectedItems={selectedObjects}
onItemSelected={this._selectObject}
renamedItem={renamedObjectWithContext}
onRename={this._rename}
onSortStart={this._onStartDraggingObject}
onSortEnd={({ oldIndex, newIndex }) => {
this.props.onEndDraggingObject();
this._move(oldIndex, newIndex);
}}
helperClass="sortable-helper"
distance={20}
buildMenuTemplate={this._renderObjectMenuTemplate}
onMoveSelectionToItem={this._moveSelectionTo}
canMoveSelectionToItem={this._canMoveSelectionTo}
reactDndType={objectWithContextReactDndType}
/>
)}
</AutoSizer>

View File

@@ -28,6 +28,8 @@ const styles = {
},
};
const getResourceName = (resource: gdResource) => resource.getName();
type State = {|
renamedResource: ?gdResource,
searchText: string,
@@ -37,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,
@@ -164,7 +166,9 @@ export default class ResourcesList extends React.Component<Props, State> {
{
renamedResource: resource,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
() => {
if (this.sortableList) this.sortableList.forceUpdateGrid();
}
);
};
@@ -194,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) => {
@@ -348,17 +357,15 @@ export default class ResourcesList extends React.Component<Props, State> {
fullList={filteredList}
width={width}
height={height}
selectedItem={selectedResource}
getItemName={getResourceName}
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

@@ -36,7 +36,6 @@ import InfoBar from '../UI/Messages/InfoBar';
import ContextMenu from '../UI/Menu/ContextMenu';
import { showWarningBox } from '../UI/Messages/MessageBox';
import { shortenString } from '../Utils/StringHelpers';
import { roundPosition } from '../Utils/GridHelpers';
import getObjectByName from '../Utils/GetObjectByName';
import {
@@ -61,7 +60,11 @@ import {
} from '../ObjectsList/EnumerateObjects';
import TagsButton from '../UI/EditorMosaic/TagsButton';
import CloseButton from '../UI/EditorMosaic/CloseButton';
import { buildTagsMenuTemplate, getTagsFromString } from '../Utils/TagsHelper';
import {
type SelectedTags,
buildTagsMenuTemplate,
getTagsFromString,
} from '../Utils/TagsHelper';
const gd = global.gd;
const INSTANCES_CLIPBOARD_KIND = 'Instances';
@@ -124,14 +127,10 @@ type State = {|
variablesEditedInstance: ?gdInitialInstance,
variablesEditedObject: ?gdObject,
selectedObjectNames: Array<string>,
newObjectInstancePosition: ?[number, number],
newObjectInstanceSceneCoordinates: ?[number, number],
editedGroup: ?gdObjectGroup,
// State for "drag'n'dropping" from the objects list to the instances editor:
objectDraggedFromList: ?gdObject,
canDropDraggedObject: boolean,
uiSettings: Object,
history: HistoryState,
@@ -140,7 +139,7 @@ type State = {|
showPropertiesInfoBar: boolean,
// State for tags of objects:
selectedObjectTags: Array<string>,
selectedObjectTags: SelectedTags,
|};
type CopyCutPasteOptions = { useLastCursorPosition?: boolean };
@@ -176,13 +175,9 @@ export default class SceneEditor extends React.Component<Props, State> {
variablesEditedInstance: null,
variablesEditedObject: null,
selectedObjectNames: [],
newObjectInstancePosition: null,
newObjectInstanceSceneCoordinates: 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,
@@ -358,77 +353,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 });
};
@@ -491,7 +415,7 @@ export default class SceneEditor extends React.Component<Props, State> {
// Remember where to create the instance, when the object will be created
this.setState({
newObjectInstancePosition: this.editor.getLastCursorPosition(),
newObjectInstanceSceneCoordinates: this.editor.getLastCursorSceneCoordinates(),
});
if (this._objectsList)
@@ -504,36 +428,17 @@ export default class SceneEditor extends React.Component<Props, State> {
}
const objectSelected = this.state.selectedObjectNames[0];
const cursorPosition = this.editor.getLastCursorPosition();
this._addInstance(cursorPosition[0], cursorPosition[1], objectSelected);
const cursorPosition = this.editor.getLastCursorSceneCoordinates();
this._addInstance(cursorPosition, objectSelected);
this.setState({
selectedObjectNames: [objectSelected],
});
};
_addInstance = (x: number, y: number, objectName: string) => {
if (!objectName) return;
_addInstance = (pos: [number, number], objectName: string) => {
if (!objectName || !this.editor) return;
const instance = this.props.initialInstances.insertNewInitialInstance();
instance.setObjectName(objectName);
if (this.state.uiSettings.grid) {
x = roundPosition(
x,
this.state.uiSettings.gridWidth,
this.state.uiSettings.gridOffsetX
);
y = roundPosition(
y,
this.state.uiSettings.gridHeight,
this.state.uiSettings.gridOffsetY
);
}
instance.setX(x);
instance.setY(y);
this.props.initialInstances.iterateOverInstances(this.zOrderFinder);
if (this.zOrderFinder) {
instance.setZOrder(this.zOrderFinder.getHighestZOrder() + 1);
}
this.editor.addInstances(pos, [objectName]);
this.setState(
{
selectedObjectNames: [],
@@ -600,20 +505,16 @@ export default class SceneEditor extends React.Component<Props, State> {
/**
* Create an instance of the given object, at the position
* previously chosen (see `newObjectInstancePosition`).
* previously chosen (see `newObjectInstanceSceneCoordinates`).
*/
_addNewObjectInstance = (newObjectName: string) => {
const { newObjectInstancePosition } = this.state;
if (!newObjectInstancePosition) {
_addInstanceForNewObject = (newObjectName: string) => {
const { newObjectInstanceSceneCoordinates } = this.state;
if (!newObjectInstanceSceneCoordinates) {
return;
}
this._addInstance(
newObjectInstancePosition[0],
newObjectInstancePosition[1],
newObjectName
);
this.setState({ newObjectInstancePosition: null });
this._addInstance(newObjectInstanceSceneCoordinates, newObjectName);
this.setState({ newObjectInstanceSceneCoordinates: null });
};
_onRemoveLayer = (layerName: string, done: boolean => void) => {
@@ -784,11 +685,6 @@ export default class SceneEditor extends React.Component<Props, State> {
done(true);
};
_canGroupUseNewName = (groupWithScope: GroupWithContext, newName: string) => {
//TODO: implement and launch refactoring (using gd.WholeProjectRefactorer but only on some events)
return true;
};
_onRenameGroup = (
groupWithContext: GroupWithContext,
newName: string,
@@ -870,8 +766,8 @@ export default class SceneEditor extends React.Component<Props, State> {
if (this.editor) {
const position = useLastCursorPosition
? this.editor.getLastCursorPosition()
: this.editor.getLastContextMenuPosition();
? this.editor.getLastCursorSceneCoordinates()
: this.editor.getLastContextMenuSceneCoordinates();
Clipboard.set(INSTANCES_CLIPBOARD_KIND, {
x: position[0],
y: position[1],
@@ -890,8 +786,8 @@ export default class SceneEditor extends React.Component<Props, State> {
if (!clipboardContent || !this.editor) return;
const position = useLastCursorPosition
? this.editor.getLastCursorPosition()
: this.editor.getLastContextMenuPosition();
? this.editor.getLastCursorSceneCoordinates()
: this.editor.getLastContextMenuSceneCoordinates();
const { x, y } = clipboardContent;
clipboardContent.instances
.map(serializedInstance => {
@@ -997,7 +893,6 @@ export default class SceneEditor extends React.Component<Props, State> {
project={project}
layout={layout}
initialInstances={initialInstances}
onAddInstance={this._addInstance}
options={this.state.uiSettings}
onChangeOptions={this.setUiSettings}
instancesSelection={this.instancesSelection}
@@ -1006,9 +901,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 })}
@@ -1017,7 +910,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}
/>
@@ -1048,13 +940,10 @@ export default class SceneEditor extends React.Component<Props, State> {
onEditObject={this.props.onEditObject || this.editObject}
onDeleteObject={this._onDeleteObject}
canRenameObject={this._canObjectOrGroupUseNewName}
onObjectCreated={this._addNewObjectInstance}
onObjectCreated={this._addInstanceForNewObject}
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,11 +1,12 @@
import { Component } from 'react';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import { DragDropContext } from 'react-dnd';
class DragDropContextProvider extends Component {
class DragAndDropContextProvider extends Component {
render() {
return this.props.children;
}
}
export default DragDropContext(HTML5Backend)(DragDropContextProvider);
export default DragDropContext(HTML5Backend)(DragAndDropContextProvider);

View File

@@ -0,0 +1,104 @@
// @flow
import * as React from 'react';
import {
DragSource,
type DragSourceMonitor,
type DragSourceConnector,
type ConnectDragSource,
DropTarget,
type DropTargetMonitor,
type DropTargetConnector,
type ConnectDropTarget,
} from 'react-dnd';
type Props<DraggedItemType> = {|
children: ({
connectDragSource: ConnectDragSource,
connectDropTarget: ConnectDropTarget,
isOver: boolean,
canDrop: boolean,
}) => React.Node,
beginDrag: () => DraggedItemType,
canDrop: (item: DraggedItemType) => boolean,
drop: () => void,
|};
type DragSourceProps = {|
connectDragSource: ConnectDragSource,
|};
type DropTargetProps = {|
connectDropTarget: ConnectDropTarget,
isOver: boolean,
canDrop: boolean,
|};
type InnerDragSourceAndDropTargetProps<DraggedItemType> = {|
...Props<DraggedItemType>,
...DragSourceProps,
...DropTargetProps,
|};
export const makeDragSourceAndDropTarget = <DraggedItemType>(
reactDndType: string
): ((Props<DraggedItemType>) => React.Node) => {
const sourceSpec = {
beginDrag(props: InnerDragSourceAndDropTargetProps<DraggedItemType>) {
return props.beginDrag();
},
};
function sourceCollect(
connect: DragSourceConnector,
monitor: DragSourceMonitor
): DragSourceProps {
return {
connectDragSource: connect.dragSource(),
};
}
const targetSpec = {
canDrop(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
const item = monitor.getItem();
return item && props.canDrop(item);
},
drop(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
if (monitor.didDrop()) {
return; // Drop already handled by another target
}
props.drop();
},
};
function targetCollect(
connect: DropTargetConnector,
monitor: DropTargetMonitor
): DropTargetProps {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
};
}
const InnerDragSourceAndDropTarget = DragSource(
reactDndType,
sourceSpec,
sourceCollect
)(
DropTarget(reactDndType, targetSpec, targetCollect)(
({ children, connectDragSource, connectDropTarget, isOver, canDrop }) => {
return children({
connectDragSource,
connectDropTarget,
isOver,
canDrop,
});
}
)
);
return (props: Props<DraggedItemType>) => (
<InnerDragSourceAndDropTarget {...props} />
);
};

View File

@@ -0,0 +1,68 @@
// @flow
import * as React from 'react';
import {
DropTarget,
type DropTargetMonitor,
type DropTargetConnector,
type ConnectDropTarget,
} from 'react-dnd';
type Props<DraggedItemType> = {|
children: ({
connectDropTarget: ConnectDropTarget,
isOver: boolean,
canDrop: boolean,
}) => React.Node,
canDrop: (item: DraggedItemType) => boolean,
hover?: (monitor: DropTargetMonitor) => void,
drop: (monitor: DropTargetMonitor) => void,
|};
type DropTargetProps = {|
connectDropTarget: ConnectDropTarget,
isOver: boolean,
canDrop: boolean,
|};
export const makeDropTarget = <DraggedItemType>(
reactDndType: string
): ((Props<DraggedItemType>) => React.Node) => {
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(monitor);
},
};
function targetCollect(
connect: DropTargetConnector,
monitor: DropTargetMonitor
): DropTargetProps {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
};
}
const InnerDropTarget = DropTarget(reactDndType, targetSpec, targetCollect)(
({ children, connectDropTarget, isOver, canDrop }) => {
return children({
connectDropTarget,
isOver,
canDrop,
});
}
);
return (props: Props<DraggedItemType>) => <InnerDropTarget {...props} />;
};

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 = {
@@ -17,23 +16,26 @@ const styles = {
},
};
type Props = {
index: number,
const LEFT_MOUSE_BUTTON = 0;
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>,
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();
@@ -42,12 +44,20 @@ class ItemRow extends React.Component<Props, *> {
}
render() {
const { item, selected, style, getThumbnail, errorStatus } = this.props;
const {
item,
itemName,
isBold,
selected,
getThumbnail,
errorStatus,
onEdit,
onItemSelected,
} = this.props;
return (
<ThemeConsumer>
{muiTheme => {
const itemName = item.getName();
const label = this.props.editingName ? (
<TextField
id="rename-item-field"
@@ -71,6 +81,8 @@ class ItemRow extends React.Component<Props, *> {
color: selected
? muiTheme.listItem.selectedTextColor
: undefined,
fontStyle: isBold ? 'italic' : undefined,
fontWeight: isBold ? 'bold' : 'normal',
}}
>
{itemName}
@@ -96,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()} />
@@ -104,10 +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 (!onEdit) return;
if (this.props.editingName) return;
onItemSelected(null);
onEdit(item);
}}
/>
);

View File

@@ -3,52 +3,41 @@ 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,
getName: () => string,
};
type ItemsListProps = {
type Props<Item> = {|
height: number,
width: number,
fullList: Array<Item>,
selectedItem: ?Item,
selectedItems: Array<Item>,
onAddNewItem?: () => void,
addNewItemLabel?: React.Node | string,
onRename: (Item, string) => void,
getThumbnail?: Item => string,
getItemName: Item => string,
getItemThumbnail?: Item => string,
isItemBold?: Item => boolean,
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, *> {
list: any;
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();
if (this._list) this._list.forceUpdateGrid();
}
render() {
@@ -56,56 +45,105 @@ class ItemsList extends React.Component<ItemsListProps, *> {
height,
width,
fullList,
selectedItem,
selectedItems,
addNewItemLabel,
renamedItem,
getThumbnail,
getItemThumbnail,
getItemName,
erroredItems,
onAddNewItem,
isItemBold,
onEditItem,
onMoveSelectionToItem,
canMoveSelectionToItem,
} = this.props;
const { DragSourceAndDropTarget } = this;
return (
<List
ref={list => (this.list = list)}
ref={list => (this._list = list)}
height={height}
rowCount={fullList.length}
rowCount={fullList.length + (onAddNewItem ? 1 : 0)}
rowHeight={
getThumbnail ? listItemWith32PxIconHeight : listItemWithoutIconHeight
getItemThumbnail
? listItemWith32PxIconHeight
: listItemWithoutIconHeight
}
rowRenderer={({ index, key, style }) => {
const item = fullList[index];
if (item.key === 'add-item-row') {
rowRenderer={({
index,
key,
style,
}: {|
index: number,
key: string,
style: Object,
|}) => {
if (index >= fullList.length) {
return (
<SortableAddItemRow
index={fullList.length}
key={key}
style={style}
disabled
onClick={this.props.onAddNewItem}
primaryText={addNewItemLabel}
/>
<div style={style} key={key}>
<AddListItem
disabled
onClick={onAddNewItem}
primaryText={addNewItemLabel}
/>
</div>
);
}
const item = fullList[index];
const nameBeingEdited = renamedItem === item;
const itemName = getItemName(item);
return (
<SortableItemRow
index={index}
key={key}
item={item}
style={style}
onRename={newName => this.props.onRename(item, newName)}
editingName={nameBeingEdited}
getThumbnail={getThumbnail ? () => getThumbnail(item) : undefined}
selected={item === selectedItem}
onItemSelected={this.props.onItemSelected}
errorStatus={
erroredItems ? erroredItems[item.getName()] || '' : ''
}
buildMenuTemplate={() =>
this.props.buildMenuTemplate(item, index)
}
/>
<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}
@@ -113,6 +151,3 @@ class ItemsList extends React.Component<ItemsListProps, *> {
);
}
}
const SortableItemsList = SortableContainer(ItemsList, { withRef: true });
export default SortableItemsList;

View File

@@ -7,9 +7,11 @@ import {
getProgramOpeningCount,
incrementProgramOpeningCount,
} from './LocalStats';
import { getStartupTimesSummary } from '../StartupTimes';
const isDev = Window.isDev();
let client = null;
let startupTimesSummary = null;
export const installAnalyticsEvents = (authentification: Authentification) => {
if (isDev) {
@@ -28,8 +30,12 @@ export const installAnalyticsEvents = (authentification: Authentification) => {
});
client.extendEvents(function() {
// Include the user public profile.
const userProfile = authentification.getUserProfileSync();
// Compute the startup times (only once to avoid doing this for every event).
startupTimesSummary = startupTimesSummary || getStartupTimesSummary();
return {
user: {
uuid: getUserUUID(),
@@ -41,6 +47,7 @@ export const installAnalyticsEvents = (authentification: Authentification) => {
localStats: {
programOpeningCount: getProgramOpeningCount(),
},
startupTimesSummary,
page: {
title: document.title,
url: document.location.href,

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

@@ -0,0 +1,39 @@
// @flow
type RawStepMeasure = [string, number];
type SummarizedStep = {|
stepName: string,
time: number,
elapsedTime: number,
|};
type Summary = {|
totalStartupTime: number,
steps: Array<SummarizedStep>,
|};
export const GD_STARTUP_TIMES: Array<RawStepMeasure> = global.GD_STARTUP_TIMES;
if (!GD_STARTUP_TIMES) {
console.error(
'Could not find GD_STARTUP_TIMES array. Have you declared it in index.html, in a synchronous script?'
);
}
export const getStartupTimesSummary = (): Summary => {
let previousStep = ['<init>', 0];
let steps = GD_STARTUP_TIMES.map(step => {
const stepSummary = {
stepName: step[0],
time: step[1],
elapsedTime: step[1] - previousStep[1],
};
previousStep = step;
return stepSummary;
});
return {
steps,
totalStartupTime: previousStep[1],
};
};

View File

@@ -13,6 +13,7 @@ import { unregister } from './registerServiceWorker';
import './UI/iconmoon-font.css'; // Styles for Iconmoon font.
import optionalRequire from './Utils/OptionalRequire.js';
import { showErrorBox } from './UI/Messages/MessageBox';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
// No i18n in this file
@@ -32,6 +33,7 @@ class Bootstrapper extends Component<{}, State> {
installAnalyticsEvents(this.authentification);
installRaven();
installFullstory();
GD_STARTUP_TIMES.push(["bootstrapperComponentDidMount", performance.now()]);
if (electron) {
import(/* webpackChunkName: "local-app" */ './LocalApp')
@@ -67,8 +69,13 @@ class Bootstrapper extends Component<{}, State> {
}
}
const rootElement = document.getElementById('root');
if (rootElement) ReactDOM.render(<Bootstrapper />, rootElement);
if (rootElement) {
GD_STARTUP_TIMES.push(['reactDOMRenderCall', performance.now()]);
ReactDOM.render(<Bootstrapper />, rootElement);
}
else console.error('No root element defined in index.html');
// registerServiceWorker();

View File

@@ -0,0 +1,85 @@
// @flow
import * as React from 'react';
import { makeDragSourceAndDropTarget } from '../UI/DragAndDrop/DragSourceAndDropTarget';
import { makeDropTarget } from '../UI/DragAndDrop/DropTarget';
type Props = {||};
const DragSourceAndDropTarget = makeDragSourceAndDropTarget<{
someData: string,
}>('dnd-type1');
const DragSourceAndDropTargetBox = ({ name }: {| name: string |}) => (
<DragSourceAndDropTarget
beginDrag={() => {
console.log(
'Begin dragging' + name + ', which should be added to the selection'
);
return { someData: name };
}}
canDrop={() => name.indexOf('cant-drop-here') === -1}
drop={() => {
console.log('Selection to be dropped on' + name);
}}
>
{({ connectDragSource, connectDropTarget, isOver, canDrop }) =>
connectDropTarget(
connectDragSource(
<div
style={{
backgroundColor: 'blue',
color: 'white',
height: 100,
width: 100,
margin: 20,
}}
>
This is a box called {name}.{isOver && <div>Hovered</div>}
{canDrop && <div>Can drop here</div>}
</div>
)
)
}
</DragSourceAndDropTarget>
);
const DropTarget = makeDropTarget<{
someData: string,
}>('dnd-type1');
const DropTargetBox = ({ name }: {| name: string |}) => (
<DropTarget
canDrop={() => name.indexOf('cant-drop-here') === -1}
drop={() => {
console.log('Selection to be dropped on' + name);
}}
>
{({ connectDropTarget, isOver, canDrop }) =>
connectDropTarget(
<div
style={{
backgroundColor: 'green',
color: 'white',
height: 100,
width: 100,
margin: 20,
}}
>
This is a box called {name}.{isOver && <div>Hovered</div>}
{canDrop && <div>Can drop here</div>}
</div>
)
}
</DropTarget>
);
export default (props: Props) => (
<div>
<DragSourceAndDropTargetBox name="box1" />
<DragSourceAndDropTargetBox name="box2, cant-drop-here" />
<DragSourceAndDropTargetBox name="box3" />
<DropTargetBox name="box4, drop target only" />
<DropTargetBox name="box5, drop target but cant-drop-here" />
</div>
);

View File

@@ -65,7 +65,7 @@ import muiDecorator from './ThemeDecorator';
import paperDecorator from './PaperDecorator';
import ValueStateHolder from './ValueStateHolder';
import RefGetter from './RefGetter';
import DragDropContextProvider from '../Utils/DragDropHelpers/DragDropContextProvider';
import DragAndDropContextProvider from '../UI/DragAndDrop/DragAndDropContextProvider';
import ResourcesLoader from '../ResourcesLoader';
import VariablesList from '../VariablesList';
import ExpressionSelector from '../EventsSheet/InstructionEditor/InstructionOrExpressionSelector/ExpressionSelector';
@@ -146,6 +146,7 @@ import Dialog from '../UI/Dialog';
import MiniToolbar, { MiniToolbarText } from '../UI/MiniToolbar';
import NewObjectDialog from '../ObjectsList/NewObjectDialog';
import { Column } from '../UI/Grid';
import DragAndDropTestBed from './DragAndDropTestBed';
// No i18n in this file
@@ -244,6 +245,12 @@ storiesOf('UI Building Blocks/SemiControlledTextField', module)
/>
));
storiesOf('UI Building Blocks/DragAndDrop', module).add('test bed', () => (
<DragAndDropContextProvider>
<DragAndDropTestBed />
</DragAndDropContextProvider>
));
storiesOf('UI Building Blocks/SemiControlledAutoComplete', module)
.addDecorator(muiDecorator)
.add('default, with text', () => (
@@ -716,75 +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={() => []}
<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>
)}
/>
));
@@ -1410,7 +1435,7 @@ storiesOf('StartPage', module)
storiesOf('DebuggerContent', module)
.addDecorator(muiDecorator)
.add('with data', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={550}>
<DebuggerContent
gameData={debuggerGameDataDump}
@@ -1425,10 +1450,10 @@ storiesOf('DebuggerContent', module)
profilingInProgress={false}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
))
.add('without data', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={550}>
<DebuggerContent
gameData={null}
@@ -1443,13 +1468,13 @@ storiesOf('DebuggerContent', module)
profilingInProgress={true}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
));
storiesOf('Profiler', module)
.addDecorator(muiDecorator)
.add('without profiler output', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={550}>
<Profiler
onStart={action('start profiler')}
@@ -1458,10 +1483,10 @@ storiesOf('Profiler', module)
profilingInProgress={false}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
))
.add('without profiler output, while profiling', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={550}>
<Profiler
onStart={action('start profiler')}
@@ -1470,10 +1495,10 @@ storiesOf('Profiler', module)
profilingInProgress={true}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
))
.add('with profiler output', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={550}>
<Profiler
onStart={action('start profiler')}
@@ -1482,10 +1507,10 @@ storiesOf('Profiler', module)
profilingInProgress={false}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
))
.add('with profiler output, while profiling', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={550}>
<Profiler
onStart={action('start profiler')}
@@ -1494,7 +1519,7 @@ storiesOf('Profiler', module)
profilingInProgress={true}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
));
storiesOf('MeasuresTable', module)
@@ -1532,7 +1557,7 @@ storiesOf('LayoutChooserDialog', module)
storiesOf('EventsTree', module)
.addDecorator(muiDecorator)
.add('default (no scope)', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<div className="gd-events-sheet">
<FixedHeightFlexContainer height={500}>
<EventsTree
@@ -1565,13 +1590,13 @@ storiesOf('EventsTree', module)
/>
</FixedHeightFlexContainer>
</div>
</DragDropContextProvider>
</DragAndDropContextProvider>
));
storiesOf('EventsSheet', module)
.addDecorator(muiDecorator)
.add('default (no scope)', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={500}>
<EventsSheet
project={project}
@@ -1596,10 +1621,10 @@ storiesOf('EventsSheet', module)
onCreateEventsFunction={action('create events function')}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
))
.add('empty (no events) (no scope)', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={500}>
<EventsSheet
project={project}
@@ -1624,7 +1649,7 @@ storiesOf('EventsSheet', module)
onCreateEventsFunction={action('create events function')}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
));
storiesOf('EventsSheet/EventsFunctionExtractorDialog', module)
@@ -2015,41 +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')}
selectedObjectNames={[]}
selectedObjectTags={[]}
onChangeSelectedObjectTags={selectedObjectTags => {}}
getAllObjectTags={() => []}
/>
</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')}
selectedObjectNames={[]}
selectedObjectTags={['Tag1', 'Tag2']}
onChangeSelectedObjectTags={action('on change selected object tags')}
getAllObjectTags={() => [
'Tag1',
'Tag2',
'Looooooooooong Tag 3',
'Unselected Tag 4',
]}
/>
</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)
@@ -2624,7 +2665,7 @@ storiesOf('EventsFunctionsList', module)
storiesOf('EventsFunctionsExtensionEditor/index', module)
.addDecorator(muiDecorator)
.add('default', () => (
<DragDropContextProvider>
<DragAndDropContextProvider>
<FixedHeightFlexContainer height={500}>
<EventsFunctionsExtensionEditor
project={project}
@@ -2641,7 +2682,7 @@ storiesOf('EventsFunctionsExtensionEditor/index', module)
onCreateEventsFunction={action('on create events function')}
/>
</FixedHeightFlexContainer>
</DragDropContextProvider>
</DragAndDropContextProvider>
));
storiesOf('EventsFunctionsExtensionEditor/OptionsEditorDialog', module)