mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add a button at the top of the object list to add new objects (#7111)
This commit is contained in:
@@ -21,7 +21,6 @@ import {
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import ResponsiveRaisedButton from '../UI/ResponsiveRaisedButton';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
import { type EmptyPlaceholder } from '../ObjectsList';
|
||||
import TreeView, { type TreeViewInterface } from '../UI/TreeView';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import useAlertDialog from '../UI/Alert/useAlertDialog';
|
||||
@@ -42,6 +41,12 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
type EmptyPlaceholder = {|
|
||||
+label: string,
|
||||
+isPlaceholder: true,
|
||||
+id: string,
|
||||
|};
|
||||
|
||||
type RootFolder = {|
|
||||
+label: string,
|
||||
+children: GroupWithContextList | Array<EmptyPlaceholder>,
|
||||
@@ -501,6 +506,19 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
const getRightButton = React.useCallback(
|
||||
(i18n: I18nType) => (item: TreeViewItem) =>
|
||||
item.id === sceneGroupsRootFolderId
|
||||
? {
|
||||
icon: <Add />,
|
||||
label: i18n._(t`Add a new group`),
|
||||
click: onCreateGroup,
|
||||
id: 'add-new-group-top-button',
|
||||
}
|
||||
: null,
|
||||
[onCreateGroup]
|
||||
);
|
||||
|
||||
const getTreeViewData = React.useCallback(
|
||||
(i18n: I18nType): Array<TreeViewItem> => {
|
||||
const objectGroupsList: GroupWithContextList = enumerateGroups(
|
||||
@@ -637,6 +655,7 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
|
||||
reactDndType={groupWithContextReactDndType}
|
||||
initiallyOpenedNodeIds={initiallyOpenedNodeIds}
|
||||
shouldSelectUponContextMenuOpening
|
||||
getItemRightButton={getRightButton(i18n)}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
@@ -1 +0,0 @@
|
||||
export const CLIPBOARD_KIND = 'Object';
|
414
newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js
Normal file
414
newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js
Normal file
@@ -0,0 +1,414 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import Clipboard, { SafeExtractor } from '../Utils/Clipboard';
|
||||
import { TreeViewItemContent } from '.';
|
||||
import {
|
||||
enumerateFoldersInContainer,
|
||||
enumerateFoldersInFolder,
|
||||
enumerateObjectsInFolder,
|
||||
type ObjectFolderOrObjectWithContext,
|
||||
} from './EnumerateObjectFolderOrObject';
|
||||
import {
|
||||
addSerializedObjectToObjectsContainer,
|
||||
OBJECT_CLIPBOARD_KIND,
|
||||
} from './ObjectTreeViewItemContent';
|
||||
import { renderQuickCustomizationMenuItems } from '../QuickCustomization/QuickCustomizationMenuItems';
|
||||
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
|
||||
import type { ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
export type ObjectFolderTreeViewItemCallbacks = {|
|
||||
onObjectPasted?: gdObject => void,
|
||||
onRenameObjectFolderOrObjectWithContextFinish: (
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
|
||||
newName: string,
|
||||
cb: (boolean) => void
|
||||
) => void,
|
||||
onDeleteObjects: (
|
||||
objectWithContext: ObjectWithContext[],
|
||||
cb: (boolean) => void
|
||||
) => void,
|
||||
|};
|
||||
|
||||
export type ObjectFolderTreeViewItemProps = {|
|
||||
...ObjectFolderTreeViewItemCallbacks,
|
||||
project: gdProject,
|
||||
globalObjectsContainer: gdObjectsContainer | null,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
editName: (itemId: string) => void,
|
||||
onObjectModified: (shouldForceUpdateList: boolean) => void,
|
||||
expandFolders: (
|
||||
objectFolderOrObjectWithContexts: Array<ObjectFolderOrObjectWithContext>
|
||||
) => void,
|
||||
addFolder: (items: Array<ObjectFolderOrObjectWithContext>) => void,
|
||||
onAddNewObject: (item: ObjectFolderOrObjectWithContext | null) => void,
|
||||
onMovedObjectFolderOrObjectToAnotherFolderInSameContainer: (
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext
|
||||
) => void,
|
||||
showDeleteConfirmation: (options: any) => Promise<boolean>,
|
||||
selectObjectFolderOrObjectWithContext: (
|
||||
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext
|
||||
) => void,
|
||||
forceUpdateList: () => void,
|
||||
forceUpdate: () => void,
|
||||
|};
|
||||
|
||||
export const getObjectFolderTreeViewItemId = (
|
||||
objectFolder: gdObjectFolderOrObject
|
||||
): string => {
|
||||
// Use the ptr as id since two folders can have the same name.
|
||||
// If using folder name, this would need for methods when renaming
|
||||
// the folder to keep it open.
|
||||
return `object-folder-${objectFolder.ptr}`;
|
||||
};
|
||||
|
||||
export class ObjectFolderTreeViewItemContent implements TreeViewItemContent {
|
||||
objectFolder: gdObjectFolderOrObject;
|
||||
_isGlobal: boolean;
|
||||
props: ObjectFolderTreeViewItemProps;
|
||||
|
||||
constructor(
|
||||
objectFolder: gdObjectFolderOrObject,
|
||||
isGlobal: boolean,
|
||||
props: ObjectFolderTreeViewItemProps
|
||||
) {
|
||||
this.objectFolder = objectFolder;
|
||||
this._isGlobal = isGlobal;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
getObjectFolderOrObject(): gdObjectFolderOrObject | null {
|
||||
return this.objectFolder;
|
||||
}
|
||||
|
||||
isDescendantOf(treeViewItemContent: TreeViewItemContent): boolean {
|
||||
const objectFolderOrObject = treeViewItemContent.getObjectFolderOrObject();
|
||||
return (
|
||||
!!objectFolderOrObject &&
|
||||
this.objectFolder.isADescendantOf(objectFolderOrObject)
|
||||
);
|
||||
}
|
||||
|
||||
isSibling(treeViewItemContent: TreeViewItemContent): boolean {
|
||||
const objectFolderOrObject = treeViewItemContent.getObjectFolderOrObject();
|
||||
return (
|
||||
!!objectFolderOrObject &&
|
||||
this.objectFolder.getParent() === objectFolderOrObject.getParent()
|
||||
);
|
||||
}
|
||||
|
||||
getIndex(): number {
|
||||
return this.objectFolder.getParent().getChildPosition(this.objectFolder);
|
||||
}
|
||||
|
||||
isGlobal(): boolean {
|
||||
return this._isGlobal;
|
||||
}
|
||||
|
||||
getName(): string | React.Node {
|
||||
return this.objectFolder.getFolderName();
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return getObjectFolderTreeViewItemId(this.objectFolder);
|
||||
}
|
||||
|
||||
getHtmlId(index: number): ?string {
|
||||
return `object-item-${index}`;
|
||||
}
|
||||
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return null;
|
||||
}
|
||||
|
||||
getThumbnail(): ?string {
|
||||
return 'FOLDER';
|
||||
}
|
||||
|
||||
onClick(): void {}
|
||||
|
||||
rename(newName: string): void {
|
||||
if (this.getName() === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRenameObjectFolderOrObjectWithContextFinish(
|
||||
{ objectFolderOrObject: this.objectFolder, global: this._isGlobal },
|
||||
newName,
|
||||
doRename => {
|
||||
if (!doRename) return;
|
||||
|
||||
this.props.onObjectModified(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
edit(): void {}
|
||||
|
||||
_getPasteLabel(
|
||||
i18n: I18nType,
|
||||
{
|
||||
isGlobalObject,
|
||||
isFolder,
|
||||
}: {| isGlobalObject: boolean, isFolder: boolean |}
|
||||
) {
|
||||
let translation = t`Paste`;
|
||||
if (Clipboard.has(OBJECT_CLIPBOARD_KIND)) {
|
||||
const clipboardContent = Clipboard.get(OBJECT_CLIPBOARD_KIND);
|
||||
const clipboardObjectName =
|
||||
SafeExtractor.extractStringProperty(clipboardContent, 'name') || '';
|
||||
translation = isGlobalObject
|
||||
? t`Paste ${clipboardObjectName} as a Global Object inside folder`
|
||||
: t`Paste ${clipboardObjectName} inside folder`;
|
||||
}
|
||||
return i18n._(translation);
|
||||
}
|
||||
|
||||
buildMenuTemplate(i18n: I18nType, index: number) {
|
||||
const {
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
expandFolders,
|
||||
addFolder,
|
||||
onAddNewObject,
|
||||
onMovedObjectFolderOrObjectToAnotherFolderInSameContainer,
|
||||
forceUpdate,
|
||||
} = this.props;
|
||||
|
||||
const container = this._isGlobal
|
||||
? globalObjectsContainer
|
||||
: objectsContainer;
|
||||
if (!container) {
|
||||
return [];
|
||||
}
|
||||
const folderAndPathsInContainer = enumerateFoldersInContainer(container);
|
||||
folderAndPathsInContainer.unshift({
|
||||
path: i18n._(t`Root folder`),
|
||||
folder: container.getRootFolder(),
|
||||
});
|
||||
|
||||
const filteredFolderAndPathsInContainer = folderAndPathsInContainer.filter(
|
||||
folderAndPath =>
|
||||
!folderAndPath.folder.isADescendantOf(this.objectFolder) &&
|
||||
folderAndPath.folder !== this.objectFolder
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: this._getPasteLabel(i18n, {
|
||||
isGlobalObject: this._isGlobal,
|
||||
isFolder: true,
|
||||
}),
|
||||
enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND),
|
||||
click: () => this.paste(),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: () => this.props.editName(this.getId()),
|
||||
accelerator: 'F2',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: () => this.delete(),
|
||||
accelerator: 'Backspace',
|
||||
},
|
||||
{
|
||||
label: i18n._('Move to folder'),
|
||||
submenu: filteredFolderAndPathsInContainer.map(({ folder, path }) => ({
|
||||
label: path,
|
||||
enabled: folder !== this.objectFolder.getParent(),
|
||||
click: () => {
|
||||
if (folder === this.objectFolder.getParent()) return;
|
||||
this.objectFolder
|
||||
.getParent()
|
||||
.moveObjectFolderOrObjectToAnotherFolder(
|
||||
this.objectFolder,
|
||||
folder,
|
||||
0
|
||||
);
|
||||
onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({
|
||||
objectFolderOrObject: folder,
|
||||
global: this._isGlobal,
|
||||
});
|
||||
},
|
||||
})),
|
||||
},
|
||||
...renderQuickCustomizationMenuItems({
|
||||
i18n,
|
||||
visibility: this.objectFolder.getQuickCustomizationVisibility(),
|
||||
onChangeVisibility: visibility => {
|
||||
this.objectFolder.setQuickCustomizationVisibility(visibility);
|
||||
forceUpdate();
|
||||
},
|
||||
}),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Add a new object`),
|
||||
click: () =>
|
||||
onAddNewObject({
|
||||
objectFolderOrObject: this.objectFolder,
|
||||
global: this._isGlobal,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Add a new folder`),
|
||||
click: () =>
|
||||
addFolder([
|
||||
{ objectFolderOrObject: this.objectFolder, global: this._isGlobal },
|
||||
]),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Expand all sub folders`),
|
||||
click: () => {
|
||||
const subFolders = enumerateFoldersInFolder(this.objectFolder).map(
|
||||
folderAndPath => folderAndPath.folder
|
||||
);
|
||||
expandFolders(
|
||||
[this.objectFolder, ...subFolders].map(folder => ({
|
||||
objectFolderOrObject: folder,
|
||||
global: this._isGlobal,
|
||||
}))
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
renderRightComponent(i18n: I18nType): ?React.Node {
|
||||
return null;
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this._delete();
|
||||
}
|
||||
|
||||
async _delete(): Promise<void> {
|
||||
const {
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
onObjectModified,
|
||||
forceUpdateList,
|
||||
showDeleteConfirmation,
|
||||
onDeleteObjects,
|
||||
selectObjectFolderOrObjectWithContext,
|
||||
} = this.props;
|
||||
|
||||
const objectsToDelete = enumerateObjectsInFolder(this.objectFolder);
|
||||
if (objectsToDelete.length === 0) {
|
||||
// Folder is empty or contains only empty folders.
|
||||
selectObjectFolderOrObjectWithContext(null);
|
||||
this.objectFolder.getParent().removeFolderChild(this.objectFolder);
|
||||
forceUpdateList();
|
||||
return;
|
||||
}
|
||||
|
||||
let message: MessageDescriptor;
|
||||
let title: MessageDescriptor;
|
||||
if (objectsToDelete.length === 1) {
|
||||
message = t`Are you sure you want to remove this folder and with it the object ${objectsToDelete[0].getName()}? This can't be undone.`;
|
||||
title = t`Remove folder and object`;
|
||||
} else {
|
||||
message = t`Are you sure you want to remove this folder and all its content (objects ${objectsToDelete
|
||||
.map(object => object.getName())
|
||||
.join(', ')})? This can't be undone.`;
|
||||
title = t`Remove folder and objects`;
|
||||
}
|
||||
|
||||
const answer = await showDeleteConfirmation({ message, title });
|
||||
if (!answer) return;
|
||||
|
||||
const objectsWithContext = objectsToDelete.map(object => ({
|
||||
object,
|
||||
global: this._isGlobal,
|
||||
}));
|
||||
|
||||
// TODO: Change selectedObjectFolderOrObjectWithContext so that it's easy
|
||||
// to remove an item using keyboard only and to navigate with the arrow
|
||||
// keys right after deleting it.
|
||||
selectObjectFolderOrObjectWithContext(null);
|
||||
|
||||
const folderToDelete = this.objectFolder;
|
||||
// It's important to call onDeleteObjects, because the parent might
|
||||
// have to do some refactoring/clean up work before the object is deleted
|
||||
// (typically, the SceneEditor will remove instances referring to the object,
|
||||
// leading to the removal of their renderer - which can keep a reference to
|
||||
// the object).
|
||||
onDeleteObjects(objectsWithContext, doRemove => {
|
||||
if (!doRemove) return;
|
||||
const container = this._isGlobal
|
||||
? globalObjectsContainer
|
||||
: objectsContainer;
|
||||
if (container) {
|
||||
objectsToDelete.forEach(object => {
|
||||
container.removeObject(object.getName());
|
||||
});
|
||||
}
|
||||
|
||||
folderToDelete.getParent().removeFolderChild(folderToDelete);
|
||||
forceUpdateList();
|
||||
|
||||
onObjectModified(false);
|
||||
});
|
||||
}
|
||||
|
||||
copy(): void {}
|
||||
|
||||
cut(): void {}
|
||||
|
||||
paste(): void {
|
||||
if (!Clipboard.has(OBJECT_CLIPBOARD_KIND)) return;
|
||||
|
||||
const clipboardContent = Clipboard.get(OBJECT_CLIPBOARD_KIND);
|
||||
const serializedObject = SafeExtractor.extractObjectProperty(
|
||||
clipboardContent,
|
||||
'object'
|
||||
);
|
||||
const objectName = SafeExtractor.extractStringProperty(
|
||||
clipboardContent,
|
||||
'name'
|
||||
);
|
||||
const objectType = SafeExtractor.extractStringProperty(
|
||||
clipboardContent,
|
||||
'type'
|
||||
);
|
||||
if (!objectName || !objectType || !serializedObject) return;
|
||||
|
||||
const {
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
onObjectPasted,
|
||||
expandFolders,
|
||||
onObjectModified,
|
||||
} = this.props;
|
||||
|
||||
const newObjectWithContext = addSerializedObjectToObjectsContainer({
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectName,
|
||||
positionObjectFolderOrObjectWithContext: {
|
||||
objectFolderOrObject: this.objectFolder,
|
||||
global: this._isGlobal,
|
||||
},
|
||||
objectType,
|
||||
serializedObject,
|
||||
addInsideFolder: true,
|
||||
});
|
||||
|
||||
onObjectModified(false);
|
||||
if (onObjectPasted) onObjectPasted(newObjectWithContext.object);
|
||||
expandFolders([
|
||||
{ objectFolderOrObject: this.objectFolder, global: this._isGlobal },
|
||||
]);
|
||||
}
|
||||
|
||||
duplicate(): void {}
|
||||
|
||||
getRightButton(i18n: I18nType) {
|
||||
return null;
|
||||
}
|
||||
}
|
577
newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js
Normal file
577
newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js
Normal file
@@ -0,0 +1,577 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import Clipboard, { SafeExtractor } from '../Utils/Clipboard';
|
||||
import {
|
||||
serializeToJSObject,
|
||||
unserializeFromJSObject,
|
||||
} from '../Utils/Serializer';
|
||||
import { TreeViewItemContent } from '.';
|
||||
import { canSwapAssetOfObject } from '../AssetStore/AssetSwapper';
|
||||
import { getInstanceCountInLayoutForObject } from '../Utils/Layout';
|
||||
import {
|
||||
enumerateFoldersInContainer,
|
||||
type ObjectFolderOrObjectWithContext,
|
||||
} from './EnumerateObjectFolderOrObject';
|
||||
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
|
||||
import type { ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export const OBJECT_CLIPBOARD_KIND = 'Object';
|
||||
|
||||
export const getObjectTreeViewItemId = (object: gdObject): string => {
|
||||
// Use the ptr to avoid display bugs in the rare case a user set an object
|
||||
// as global although another layout has an object with the same name,
|
||||
// and ignored the warning.
|
||||
return `${object.getName()}-${object.ptr}`;
|
||||
};
|
||||
|
||||
export type ObjectTreeViewItemCallbacks = {|
|
||||
onObjectPasted?: gdObject => void,
|
||||
onSelectAllInstancesOfObjectInLayout?: string => void,
|
||||
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
|
||||
onDeleteObjects: (
|
||||
objectWithContext: ObjectWithContext[],
|
||||
cb: (boolean) => void
|
||||
) => void,
|
||||
onAddObjectInstance: (objectName: string) => void,
|
||||
onOpenEventBasedObjectEditor: (
|
||||
extensionName: string,
|
||||
eventsBasedObjectName: string
|
||||
) => void,
|
||||
onRenameObjectFolderOrObjectWithContextFinish: (
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
|
||||
newName: string,
|
||||
cb: (boolean) => void
|
||||
) => void,
|
||||
getValidatedObjectOrGroupName: (newName: string, global: boolean) => string,
|
||||
getThumbnail: (
|
||||
project: gdProject,
|
||||
objectConfiguration: gdObjectConfiguration
|
||||
) => string,
|
||||
|};
|
||||
|
||||
export type ObjectTreeViewItemProps = {|
|
||||
...ObjectTreeViewItemCallbacks,
|
||||
project: gdProject,
|
||||
globalObjectsContainer: gdObjectsContainer | null,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
swapObjectAsset: (objectWithContext: ObjectWithContext) => void,
|
||||
initialInstances?: gdInitialInstancesContainer,
|
||||
editName: (itemId: string) => void,
|
||||
onObjectModified: (shouldForceUpdateList: boolean) => void,
|
||||
onMovedObjectFolderOrObjectToAnotherFolderInSameContainer: (
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext
|
||||
) => void,
|
||||
canSetAsGlobalObject?: boolean,
|
||||
setAsGlobalObject: ({|
|
||||
i18n: I18nType,
|
||||
objectFolderOrObject: gdObjectFolderOrObject,
|
||||
index?: number,
|
||||
folder?: gdObjectFolderOrObject,
|
||||
|}) => void,
|
||||
showDeleteConfirmation: (options: any) => Promise<boolean>,
|
||||
selectObjectFolderOrObjectWithContext: (
|
||||
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext
|
||||
) => void,
|
||||
forceUpdateList: () => void,
|
||||
forceUpdate: () => void,
|
||||
|};
|
||||
|
||||
export const addSerializedObjectToObjectsContainer = ({
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectName,
|
||||
positionObjectFolderOrObjectWithContext,
|
||||
objectType,
|
||||
serializedObject,
|
||||
addInsideFolder,
|
||||
}: {|
|
||||
project: gdProject,
|
||||
globalObjectsContainer: gdObjectsContainer | null,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
objectName: string,
|
||||
positionObjectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
|
||||
objectType: string,
|
||||
serializedObject: Object,
|
||||
addInsideFolder?: boolean,
|
||||
|}): ObjectWithContext => {
|
||||
const newName = newNameGenerator(
|
||||
objectName,
|
||||
name =>
|
||||
objectsContainer.hasObjectNamed(name) ||
|
||||
(!!globalObjectsContainer && globalObjectsContainer.hasObjectNamed(name)),
|
||||
''
|
||||
);
|
||||
|
||||
const {
|
||||
objectFolderOrObject,
|
||||
global,
|
||||
} = positionObjectFolderOrObjectWithContext;
|
||||
let positionFolder, positionInFolder;
|
||||
if (addInsideFolder && objectFolderOrObject.isFolder()) {
|
||||
positionFolder = objectFolderOrObject;
|
||||
positionInFolder = objectFolderOrObject.getChildrenCount();
|
||||
} else {
|
||||
positionFolder = objectFolderOrObject.getParent();
|
||||
positionInFolder = positionFolder.getChildPosition(objectFolderOrObject);
|
||||
}
|
||||
|
||||
const newObject =
|
||||
global && globalObjectsContainer
|
||||
? globalObjectsContainer.insertNewObjectInFolder(
|
||||
project,
|
||||
objectType,
|
||||
newName,
|
||||
positionFolder,
|
||||
positionInFolder + 1
|
||||
)
|
||||
: objectsContainer.insertNewObjectInFolder(
|
||||
project,
|
||||
objectType,
|
||||
newName,
|
||||
positionFolder,
|
||||
positionInFolder + 1
|
||||
);
|
||||
|
||||
unserializeFromJSObject(
|
||||
newObject,
|
||||
serializedObject,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
newObject.setName(newName); // Unserialization has overwritten the name.
|
||||
|
||||
return { object: newObject, global };
|
||||
};
|
||||
|
||||
export class ObjectTreeViewItemContent implements TreeViewItemContent {
|
||||
object: gdObjectFolderOrObject;
|
||||
_isGlobal: boolean;
|
||||
props: ObjectTreeViewItemProps;
|
||||
|
||||
constructor(
|
||||
object: gdObjectFolderOrObject,
|
||||
isGlobal: boolean,
|
||||
props: ObjectTreeViewItemProps
|
||||
) {
|
||||
this.object = object;
|
||||
this._isGlobal = isGlobal;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
getObjectFolderOrObject(): gdObjectFolderOrObject | null {
|
||||
return this.object;
|
||||
}
|
||||
|
||||
isDescendantOf(treeViewItemContent: TreeViewItemContent): boolean {
|
||||
const objectFolderOrObject = treeViewItemContent.getObjectFolderOrObject();
|
||||
return (
|
||||
!!objectFolderOrObject &&
|
||||
this.object.isADescendantOf(objectFolderOrObject)
|
||||
);
|
||||
}
|
||||
|
||||
isSibling(treeViewItemContent: TreeViewItemContent): boolean {
|
||||
const objectFolderOrObject = treeViewItemContent.getObjectFolderOrObject();
|
||||
return (
|
||||
!!objectFolderOrObject &&
|
||||
this.object.getParent() === objectFolderOrObject.getParent()
|
||||
);
|
||||
}
|
||||
|
||||
getIndex(): number {
|
||||
return this.object.getParent().getChildPosition(this.object);
|
||||
}
|
||||
|
||||
isGlobal(): boolean {
|
||||
return this._isGlobal;
|
||||
}
|
||||
|
||||
getName(): string | React.Node {
|
||||
return this.object.getObject().getName();
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return getObjectTreeViewItemId(this.object.getObject());
|
||||
}
|
||||
|
||||
getHtmlId(index: number): ?string {
|
||||
return `object-item-${index}`;
|
||||
}
|
||||
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return {
|
||||
objectName: this.object.getObject().getName(),
|
||||
global: this._isGlobal.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
getThumbnail(): ?string {
|
||||
return this.props.getThumbnail(
|
||||
this.props.project,
|
||||
this.object.getObject().getConfiguration()
|
||||
);
|
||||
}
|
||||
|
||||
onClick(): void {}
|
||||
|
||||
rename(newName: string): void {
|
||||
if (this.getName() === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedNewName = this.props.getValidatedObjectOrGroupName(
|
||||
newName,
|
||||
this._isGlobal
|
||||
);
|
||||
this.props.onRenameObjectFolderOrObjectWithContextFinish(
|
||||
{ objectFolderOrObject: this.object, global: this._isGlobal },
|
||||
validatedNewName,
|
||||
doRename => {
|
||||
if (!doRename) return;
|
||||
|
||||
this.props.onObjectModified(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
this.props.onEditObject(this.object.getObject());
|
||||
}
|
||||
|
||||
_getPasteLabel(
|
||||
i18n: I18nType,
|
||||
{
|
||||
isGlobalObject,
|
||||
isFolder,
|
||||
}: {| isGlobalObject: boolean, isFolder: boolean |}
|
||||
) {
|
||||
let translation = t`Paste`;
|
||||
if (Clipboard.has(OBJECT_CLIPBOARD_KIND)) {
|
||||
const clipboardContent = Clipboard.get(OBJECT_CLIPBOARD_KIND);
|
||||
const clipboardObjectName =
|
||||
SafeExtractor.extractStringProperty(clipboardContent, 'name') || '';
|
||||
translation = isGlobalObject
|
||||
? t`Paste ${clipboardObjectName} as a Global Object`
|
||||
: t`Paste ${clipboardObjectName}`;
|
||||
}
|
||||
return i18n._(translation);
|
||||
}
|
||||
|
||||
buildMenuTemplate(i18n: I18nType, index: number) {
|
||||
const {
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
initialInstances,
|
||||
onSelectAllInstancesOfObjectInLayout,
|
||||
onEditObject,
|
||||
onMovedObjectFolderOrObjectToAnotherFolderInSameContainer,
|
||||
onAddObjectInstance,
|
||||
swapObjectAsset,
|
||||
canSetAsGlobalObject,
|
||||
setAsGlobalObject,
|
||||
onOpenEventBasedObjectEditor,
|
||||
selectObjectFolderOrObjectWithContext,
|
||||
} = this.props;
|
||||
|
||||
const container = this._isGlobal
|
||||
? globalObjectsContainer
|
||||
: objectsContainer;
|
||||
if (!container) {
|
||||
return [];
|
||||
}
|
||||
const folderAndPathsInContainer = enumerateFoldersInContainer(container);
|
||||
folderAndPathsInContainer.unshift({
|
||||
path: i18n._(t`Root folder`),
|
||||
folder: container.getRootFolder(),
|
||||
});
|
||||
|
||||
const object = this.object.getObject();
|
||||
const instanceCountOnScene = initialInstances
|
||||
? getInstanceCountInLayoutForObject(initialInstances, object.getName())
|
||||
: undefined;
|
||||
const objectMetadata = gd.MetadataProvider.getObjectMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
object.getType()
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: i18n._(t`Copy`),
|
||||
click: () => this.copy(),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Cut`),
|
||||
click: () => this.cut(),
|
||||
},
|
||||
{
|
||||
label: this._getPasteLabel(i18n, {
|
||||
isGlobalObject: this._isGlobal,
|
||||
isFolder: false,
|
||||
}),
|
||||
enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND),
|
||||
click: () => this.paste(),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Duplicate`),
|
||||
click: () => this.duplicate(),
|
||||
accelerator: 'CmdOrCtrl+D',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Rename`),
|
||||
click: () => this.props.editName(this.getId()),
|
||||
accelerator: 'F2',
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
click: () => this.delete(),
|
||||
accelerator: 'Backspace',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Edit object`),
|
||||
click: () => onEditObject(object),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Edit object variables`),
|
||||
click: () => onEditObject(object, 'variables'),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Edit behaviors`),
|
||||
click: () => onEditObject(object, 'behaviors'),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Edit effects`),
|
||||
click: () => onEditObject(object, 'effects'),
|
||||
enabled: objectMetadata.hasDefaultBehavior(
|
||||
'EffectCapability::EffectBehavior'
|
||||
),
|
||||
},
|
||||
project.hasEventsBasedObject(object.getType())
|
||||
? {
|
||||
label: i18n._(t`Edit children`),
|
||||
click: () =>
|
||||
onOpenEventBasedObjectEditor(
|
||||
gd.PlatformExtension.getExtensionFromFullObjectType(
|
||||
object.getType()
|
||||
),
|
||||
gd.PlatformExtension.getObjectNameFromFullObjectType(
|
||||
object.getType()
|
||||
)
|
||||
),
|
||||
}
|
||||
: null,
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Swap assets`),
|
||||
click: () =>
|
||||
swapObjectAsset({
|
||||
object: this.object.getObject(),
|
||||
global: this._isGlobal,
|
||||
}),
|
||||
enabled: canSwapAssetOfObject(object),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
globalObjectsContainer && {
|
||||
label: i18n._(t`Set as global object`),
|
||||
enabled: !this._isGlobal,
|
||||
click: () => {
|
||||
selectObjectFolderOrObjectWithContext(null);
|
||||
setAsGlobalObject({ i18n, objectFolderOrObject: this.object });
|
||||
},
|
||||
visible: canSetAsGlobalObject !== false,
|
||||
},
|
||||
{
|
||||
label: i18n._('Move to folder'),
|
||||
submenu: folderAndPathsInContainer.map(({ folder, path }) => ({
|
||||
label: path,
|
||||
enabled: folder !== this.object.getParent(),
|
||||
click: () => {
|
||||
this.object
|
||||
.getParent()
|
||||
.moveObjectFolderOrObjectToAnotherFolder(this.object, folder, 0);
|
||||
onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({
|
||||
objectFolderOrObject: folder,
|
||||
global: this._isGlobal,
|
||||
});
|
||||
},
|
||||
})),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Add instance to the scene`),
|
||||
click: () => onAddObjectInstance(object.getName()),
|
||||
},
|
||||
instanceCountOnScene !== undefined && onSelectAllInstancesOfObjectInLayout
|
||||
? {
|
||||
label: i18n._(
|
||||
t`Select instances on scene (${instanceCountOnScene})`
|
||||
),
|
||||
click: () => onSelectAllInstancesOfObjectInLayout(object.getName()),
|
||||
enabled: instanceCountOnScene > 0,
|
||||
}
|
||||
: undefined,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
renderRightComponent(i18n: I18nType): ?React.Node {
|
||||
return null;
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this._delete();
|
||||
}
|
||||
|
||||
async _delete(): Promise<void> {
|
||||
const {
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
onObjectModified,
|
||||
showDeleteConfirmation,
|
||||
onDeleteObjects,
|
||||
selectObjectFolderOrObjectWithContext,
|
||||
} = this.props;
|
||||
|
||||
const answer = await showDeleteConfirmation({
|
||||
title: t`Remove object`,
|
||||
message: t`Are you sure you want to remove this object? This can't be undone.`,
|
||||
});
|
||||
if (!answer) return;
|
||||
|
||||
const objectsToDelete = [this.object.getObject()];
|
||||
const objectsWithContext = objectsToDelete.map(object => ({
|
||||
object,
|
||||
global: this._isGlobal,
|
||||
}));
|
||||
|
||||
// TODO: Change selectedObjectFolderOrObjectWithContext so that it's easy
|
||||
// to remove an item using keyboard only and to navigate with the arrow
|
||||
// keys right after deleting it.
|
||||
selectObjectFolderOrObjectWithContext(null);
|
||||
|
||||
// It's important to call onDeleteObjects, because the parent might
|
||||
// have to do some refactoring/clean up work before the object is deleted
|
||||
// (typically, the SceneEditor will remove instances referring to the object,
|
||||
// leading to the removal of their renderer - which can keep a reference to
|
||||
// the object).
|
||||
onDeleteObjects(objectsWithContext, doRemove => {
|
||||
if (!doRemove) return;
|
||||
const container = this._isGlobal
|
||||
? globalObjectsContainer
|
||||
: objectsContainer;
|
||||
if (container) {
|
||||
objectsToDelete.forEach(object => {
|
||||
container.removeObject(object.getName());
|
||||
});
|
||||
}
|
||||
onObjectModified(false);
|
||||
});
|
||||
}
|
||||
|
||||
copy(): void {
|
||||
Clipboard.set(OBJECT_CLIPBOARD_KIND, {
|
||||
type: this.object.getObject().getType(),
|
||||
name: this.object.getObject().getName(),
|
||||
object: serializeToJSObject(this.object.getObject()),
|
||||
});
|
||||
}
|
||||
|
||||
cut(): void {
|
||||
this.copy();
|
||||
// TODO It should probably not show an alert
|
||||
this.delete();
|
||||
}
|
||||
|
||||
paste(): void {
|
||||
if (!Clipboard.has(OBJECT_CLIPBOARD_KIND)) return;
|
||||
|
||||
const clipboardContent = Clipboard.get(OBJECT_CLIPBOARD_KIND);
|
||||
const serializedObject = SafeExtractor.extractObjectProperty(
|
||||
clipboardContent,
|
||||
'object'
|
||||
);
|
||||
const objectName = SafeExtractor.extractStringProperty(
|
||||
clipboardContent,
|
||||
'name'
|
||||
);
|
||||
const objectType = SafeExtractor.extractStringProperty(
|
||||
clipboardContent,
|
||||
'type'
|
||||
);
|
||||
if (!objectName || !objectType || !serializedObject) return;
|
||||
|
||||
const {
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
onObjectPasted,
|
||||
onObjectModified,
|
||||
} = this.props;
|
||||
|
||||
const newObjectWithContext = addSerializedObjectToObjectsContainer({
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectName,
|
||||
positionObjectFolderOrObjectWithContext: {
|
||||
objectFolderOrObject: this.object,
|
||||
global: this._isGlobal,
|
||||
},
|
||||
objectType,
|
||||
serializedObject,
|
||||
addInsideFolder: false,
|
||||
});
|
||||
|
||||
onObjectModified(false);
|
||||
if (onObjectPasted) onObjectPasted(newObjectWithContext.object);
|
||||
}
|
||||
|
||||
duplicate(): void {
|
||||
const {
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
forceUpdateList,
|
||||
editName,
|
||||
selectObjectFolderOrObjectWithContext,
|
||||
} = this.props;
|
||||
|
||||
const object = this.object.getObject();
|
||||
const serializedObject = serializeToJSObject(object);
|
||||
|
||||
const newObjectWithContext = addSerializedObjectToObjectsContainer({
|
||||
project,
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectName: object.getName(),
|
||||
positionObjectFolderOrObjectWithContext: {
|
||||
objectFolderOrObject: this.object,
|
||||
global: this._isGlobal,
|
||||
},
|
||||
objectType: object.getType(),
|
||||
serializedObject,
|
||||
});
|
||||
|
||||
const newObjectFolderOrObjectWithContext = {
|
||||
objectFolderOrObject: this.object
|
||||
.getParent()
|
||||
.getObjectChild(newObjectWithContext.object.getName()),
|
||||
global: this._isGlobal,
|
||||
};
|
||||
|
||||
forceUpdateList();
|
||||
editName(getObjectTreeViewItemId(newObjectWithContext.object));
|
||||
selectObjectFolderOrObjectWithContext(newObjectFolderOrObjectWithContext);
|
||||
}
|
||||
|
||||
getRightButton(i18n: I18nType) {
|
||||
return null;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import {
|
||||
extensionsRootFolderId,
|
||||
} from '.';
|
||||
import { isExtensionNameTaken } from './EventFunctionExtensionNameVerifier';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
const EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND = 'Events Functions Extension';
|
||||
|
||||
@@ -78,7 +79,7 @@ export class ExtensionTreeViewItemContent implements TreeViewItemContent {
|
||||
return `extension-item-${index}`;
|
||||
}
|
||||
|
||||
getDataSet(): { [string]: string } {
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return {
|
||||
extension: this.eventsFunctionsExtension.getName(),
|
||||
};
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
type TreeItemProps,
|
||||
externalEventsRootFolderId,
|
||||
} from '.';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
const EXTERNAL_EVENTS_CLIPBOARD_KIND = 'External events';
|
||||
|
||||
@@ -73,7 +74,7 @@ export class ExternalEventsTreeViewItemContent implements TreeViewItemContent {
|
||||
return `external-events-item-${index}`;
|
||||
}
|
||||
|
||||
getDataSet(): { [string]: string } {
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return {
|
||||
'external-events': this.externalEvents.getName(),
|
||||
};
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
type TreeItemProps,
|
||||
externalLayoutsRootFolderId,
|
||||
} from '.';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
const EXTERNAL_LAYOUT_CLIPBOARD_KIND = 'External layout';
|
||||
|
||||
@@ -73,7 +74,7 @@ export class ExternalLayoutTreeViewItemContent implements TreeViewItemContent {
|
||||
return `external-layout-item-${index}`;
|
||||
}
|
||||
|
||||
getDataSet(): { [string]: string } {
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return {
|
||||
'external-layout': this.externalLayout.getName(),
|
||||
};
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
import { TreeViewItemContent, type TreeItemProps, scenesRootFolderId } from '.';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Flag from '@material-ui/icons/Flag';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
const SCENE_CLIPBOARD_KIND = 'Layout';
|
||||
|
||||
@@ -72,7 +73,7 @@ export class SceneTreeViewItemContent implements TreeViewItemContent {
|
||||
return `scene-item-${index}`;
|
||||
}
|
||||
|
||||
getDataSet(): { [string]: string } {
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return {
|
||||
scene: this.scene.getName(),
|
||||
};
|
||||
|
@@ -75,6 +75,7 @@ import { type ShowConfirmDeleteDialogOptions } from '../UI/Alert/AlertContext';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import { type GDevelopTheme } from '../UI/Theme';
|
||||
import { ExtensionStoreContext } from '../AssetStore/ExtensionStore/ExtensionStoreContext';
|
||||
import { type HTMLDataset } from '../Utils/HTMLDataset';
|
||||
|
||||
export const getProjectManagerItemId = (identifier: string) =>
|
||||
`project-manager-tab-${identifier}`;
|
||||
@@ -117,7 +118,7 @@ export interface TreeViewItemContent {
|
||||
getName(): string | React.Node;
|
||||
getId(): string;
|
||||
getHtmlId(index: number): ?string;
|
||||
getDataSet(): { [string]: string };
|
||||
getDataSet(): ?HTMLDataset;
|
||||
getThumbnail(): ?string;
|
||||
onClick(): void;
|
||||
buildMenuTemplate(i18n: I18nType, index: number): Array<MenuItemTemplate>;
|
||||
@@ -227,8 +228,8 @@ class LabelTreeViewItemContent implements TreeViewItemContent {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
getDataSet(): { [string]: string } {
|
||||
return {};
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return null;
|
||||
}
|
||||
|
||||
getThumbnail(): ?string {
|
||||
@@ -315,8 +316,8 @@ class ActionTreeViewItemContent implements TreeViewItemContent {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
getDataSet(): { [string]: string } {
|
||||
return {};
|
||||
getDataSet(): ?HTMLDataset {
|
||||
return null;
|
||||
}
|
||||
|
||||
getThumbnail(): ?string {
|
||||
|
@@ -377,9 +377,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
projectScopedContainersAccessor={projectScopedContainersAccessor}
|
||||
objectsContainer={objectsContainer}
|
||||
initialInstances={initialInstances}
|
||||
onSelectAllInstancesOfObjectInLayout={
|
||||
props.onSelectAllInstancesOfObjectInLayout
|
||||
|
@@ -307,8 +307,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
ObjectsRenderingService
|
||||
)}
|
||||
project={project}
|
||||
objectsContainer={objectsContainer}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
projectScopedContainersAccessor={
|
||||
projectScopedContainersAccessor
|
||||
}
|
||||
|
@@ -446,6 +446,7 @@ const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => {
|
||||
rightButton.click();
|
||||
}
|
||||
}}
|
||||
tooltip={rightButton.label}
|
||||
>
|
||||
{rightButton.icon}
|
||||
</IconButton>
|
||||
|
@@ -10,6 +10,7 @@ import TreeViewRow, { TREE_VIEW_ROW_HEIGHT } from './TreeViewRow';
|
||||
import { makeDragSourceAndDropTarget } from '../DragAndDrop/DragSourceAndDropTarget';
|
||||
import { type HTMLDataset } from '../../Utils/HTMLDataset';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
|
||||
|
||||
export const navigationKeys = [
|
||||
'ArrowDown',
|
||||
@@ -27,7 +28,7 @@ export type ItemBaseAttributes = {
|
||||
export type MenuButton = {|
|
||||
id?: string,
|
||||
icon: React.Node,
|
||||
label: string,
|
||||
label: MessageDescriptor,
|
||||
click: ?() => void | Promise<void>,
|
||||
|};
|
||||
|
||||
@@ -125,7 +126,9 @@ export type TreeViewInterface<Item> = {|
|
||||
openItems: (string[]) => void,
|
||||
closeItems: (string[]) => void,
|
||||
animateItem: Item => void,
|
||||
animateItemFromId: (itemId: string) => void,
|
||||
areItemsOpen: (Array<Item>) => boolean[],
|
||||
areItemsOpenFromId: (Array<string>) => boolean[],
|
||||
|};
|
||||
|
||||
type Props<Item> = {|
|
||||
@@ -485,6 +488,10 @@ const TreeView = <Item: ItemBaseAttributes>(
|
||||
[getItemId]
|
||||
);
|
||||
|
||||
const animateItemFromId = React.useCallback((itemId: string) => {
|
||||
setAnimatedItemId(itemId);
|
||||
}, []);
|
||||
|
||||
const areItemsOpen = React.useCallback(
|
||||
(items: Item[]) => {
|
||||
const itemIds = items.map(getItemId);
|
||||
@@ -494,6 +501,14 @@ const TreeView = <Item: ItemBaseAttributes>(
|
||||
[openedNodeIds, getItemId]
|
||||
);
|
||||
|
||||
const areItemsOpenFromId = React.useCallback(
|
||||
(itemIds: Array<string>) => {
|
||||
const openedNodeIdsSet = new Set(openedNodeIds);
|
||||
return itemIds.map(id => openedNodeIdsSet.has(id));
|
||||
},
|
||||
[openedNodeIds]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (animatedItemId) {
|
||||
@@ -523,7 +538,9 @@ const TreeView = <Item: ItemBaseAttributes>(
|
||||
openItems,
|
||||
closeItems,
|
||||
animateItem,
|
||||
animateItemFromId,
|
||||
areItemsOpen,
|
||||
areItemsOpenFromId,
|
||||
})
|
||||
);
|
||||
|
||||
|
@@ -264,8 +264,6 @@ export const WithObjectsList = () => (
|
||||
project={testProject.project}
|
||||
layout={testProject.testLayout}
|
||||
eventsBasedObject={null}
|
||||
globalObjectsContainer={testProject.project.getObjects()}
|
||||
objectsContainer={testProject.testLayout.getObjects()}
|
||||
projectScopedContainersAccessor={
|
||||
testProject.testSceneProjectScopedContainersAccessor
|
||||
}
|
||||
|
@@ -28,11 +28,9 @@ export const Default = () => (
|
||||
project={testProject.project}
|
||||
layout={testProject.testLayout}
|
||||
eventsBasedObject={null}
|
||||
globalObjectsContainer={testProject.project.getObjects()}
|
||||
projectScopedContainersAccessor={
|
||||
testProject.testSceneProjectScopedContainersAccessor
|
||||
}
|
||||
objectsContainer={testProject.testLayout.getObjects()}
|
||||
resourceManagementProps={fakeResourceManagementProps}
|
||||
onEditObject={action('On edit object')}
|
||||
onOpenEventBasedObjectEditor={action('On edit children')}
|
||||
@@ -64,11 +62,9 @@ export const WithSerializedObjectView = () => (
|
||||
project={testProject.project}
|
||||
layout={testProject.testLayout}
|
||||
eventsBasedObject={null}
|
||||
globalObjectsContainer={testProject.project.getObjects()}
|
||||
projectScopedContainersAccessor={
|
||||
testProject.testSceneProjectScopedContainersAccessor
|
||||
}
|
||||
objectsContainer={testProject.testLayout.getObjects()}
|
||||
resourceManagementProps={fakeResourceManagementProps}
|
||||
onEditObject={action('On edit object')}
|
||||
onOpenEventBasedObjectEditor={action('On edit children')}
|
||||
|
Reference in New Issue
Block a user