Add a button at the top of the object list to add new objects (#7111)

This commit is contained in:
D8H
2024-10-29 16:52:04 +01:00
committed by GitHub
parent b9a899f82e
commit 70e6fc7f7f
16 changed files with 1862 additions and 1106 deletions

View File

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

View File

@@ -1 +0,0 @@
export const CLIPBOARD_KIND = 'Object';

View 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;
}
}

View 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

View File

@@ -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(),
};

View File

@@ -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(),
};

View File

@@ -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(),
};

View File

@@ -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(),
};

View File

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

View File

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

View File

@@ -307,8 +307,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
ObjectsRenderingService
)}
project={project}
objectsContainer={objectsContainer}
globalObjectsContainer={globalObjectsContainer}
projectScopedContainersAccessor={
projectScopedContainersAccessor
}

View File

@@ -446,6 +446,7 @@ const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => {
rightButton.click();
}
}}
tooltip={rightButton.label}
>
{rightButton.icon}
</IconButton>

View File

@@ -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,
})
);

View File

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

View File

@@ -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')}